import XCTest
@testable import Rosetta

@MainActor
final class SchemaParityTests: XCTestCase {
    private var ctx: DBTestContext!

    override func setUpWithError() throws {
        ctx = DBTestContext()
    }

    override func tearDownWithError() throws {
        ctx.teardown()
        ctx = nil
    }

    func testSchemaContainsRequiredTablesColumnsAndIndexes() async throws {
        try await ctx.bootstrap()
        let schema = try ctx.schemaSnapshot()

        let requiredTables: Set<String> = [
            "messages",
            "dialogs",
            "accounts_sync_times",
            "sync_cursors",
            "groups",
            "pinned_messages",
            "avatar_cache",
            "blacklist",
        ]
        XCTAssertTrue(requiredTables.isSubset(of: schema.tables), "Missing required tables")

        let messagesColumns = schema.columnsByTable["messages"] ?? []
        let requiredMessageColumns: Set<String> = [
            "account", "from_public_key", "to_public_key", "message_id", "dialog_key",
            "timestamp", "is_read", "read", "delivery_status", "delivered", "text", "plain_message",
            "attachments", "reply_to_message_id",
        ]
        XCTAssertTrue(requiredMessageColumns.isSubset(of: messagesColumns), "Missing messages columns")

        let dialogsColumns = schema.columnsByTable["dialogs"] ?? []
        let requiredDialogColumns: Set<String> = [
            "account", "opponent_key", "dialog_id", "is_request", "last_timestamp", "last_message_id",
            "last_message_timestamp", "i_have_sent", "unread_count",
        ]
        XCTAssertTrue(requiredDialogColumns.isSubset(of: dialogsColumns), "Missing dialogs columns")

        let requiredIndexes: Set<String> = [
            "idx_messages_account_message_id",
            "idx_messages_account_dialog_key_timestamp",
            "idx_messages_account_dialog_fromme_isread",
            "idx_messages_account_dialog_fromme_timestamp",
            "idx_dialogs_account_opponent_key",
        ]
        XCTAssertTrue(requiredIndexes.isSubset(of: schema.indexes), "Missing required indexes")
    }

    func testUnreadAndSentQueriesUseParityIndexes() async throws {
        try await ctx.bootstrap()
        try await ctx.runScenario(FixtureScenario(name: "seed", events: [
            .incoming(opponent: "02peer_a", messageId: "m1", timestamp: 1, text: "hello"),
            .outgoing(opponent: "02peer_a", messageId: "m2", timestamp: 2, text: "yo"),
        ]))

        let sqlite = try ctx.openSQLite()
        let unreadPlanRows = try sqlite.query(
            """
            EXPLAIN QUERY PLAN
            SELECT COUNT(*) FROM messages
            WHERE account = ? AND dialog_key = ? AND from_me = 0 AND is_read = 0
            """,
            [.text(ctx.account), .text(DatabaseManager.dialogKey(account: ctx.account, opponentKey: "02peer_a"))]
        )
        let unreadPlan = unreadPlanRows.compactMap { $0["detail"] }.joined(separator: " | ")
        XCTAssertTrue(
            unreadPlan.contains("idx_messages_account_dialog_fromme_isread"),
            "Unread query plan did not use idx_messages_account_dialog_fromme_isread: \(unreadPlan)"
        )

        let sentPlanRows = try sqlite.query(
            """
            EXPLAIN QUERY PLAN
            SELECT message_id FROM messages
            WHERE account = ? AND dialog_key = ? AND from_me = 1
            ORDER BY timestamp DESC
            LIMIT 1
            """,
            [.text(ctx.account), .text(DatabaseManager.dialogKey(account: ctx.account, opponentKey: "02peer_a"))]
        )
        let sentPlan = sentPlanRows.compactMap { $0["detail"] }.joined(separator: " | ")
        XCTAssertTrue(
            sentPlan.contains("idx_messages_account_dialog_fromme_timestamp"),
            "Sent query plan did not use idx_messages_account_dialog_fromme_timestamp: \(sentPlan)"
        )
    }

    func testPacketRegistrySupportsMessagingAndGroupsCoreIds() throws {
        let packets: [(Int, any Packet)] = [
            (0x00, PacketHandshake()),
            (0x01, PacketUserInfo()),
            (0x02, PacketResult()),
            (0x03, PacketSearch()),
            (0x04, PacketOnlineSubscribe()),
            (0x05, PacketOnlineState()),
            (0x06, PacketMessage()),
            (0x07, PacketRead()),
            (0x08, PacketDelivery()),
            (0x09, PacketDeviceNew()),
            (0x0A, PacketRequestUpdate()),
            (0x0B, PacketTyping()),
            (0x0F, PacketRequestTransport()),
            (0x10, PacketPushNotification()),
            (0x11, PacketCreateGroup()),
            (0x12, PacketGroupInfo()),
            (0x13, PacketGroupInviteInfo()),
            (0x14, PacketGroupJoin()),
            (0x15, PacketGroupLeave()),
            (0x16, PacketGroupBan()),
            (0x17, PacketDeviceList()),
            (0x18, PacketDeviceResolve()),
            (0x19, PacketSync()),
        ]

        for (expectedId, packet) in packets {
            let encoded = PacketRegistry.encode(packet)
            guard let decoded = PacketRegistry.decode(from: encoded) else {
                XCTFail("Failed to decode packet 0x\(String(expectedId, radix: 16))")
                continue
            }
            XCTAssertEqual(decoded.packetId, expectedId)
        }
    }

    func testAttachmentTypeCallRoundTripDecoding() throws {
        var packet = PacketMessage()
        packet.fromPublicKey = "02from"
        packet.toPublicKey = "02to"
        packet.content = ""
        packet.chachaKey = ""
        packet.timestamp = 123
        packet.privateKey = "hash"
        packet.messageId = "msg-call"
        packet.attachments = [MessageAttachment(id: "call-1", preview: "", blob: "", type: .call)]
        packet.aesChachaKey = ""

        let encoded = PacketRegistry.encode(packet)
        guard let decoded = PacketRegistry.decode(from: encoded),
              let decodedMessage = decoded.packet as? PacketMessage
        else {
            XCTFail("Failed to decode call attachment packet")
            return
        }

        XCTAssertEqual(decoded.packetId, 0x06)
        XCTAssertEqual(decodedMessage.attachments.first?.type, .call)
        XCTAssertFalse(decodedMessage.isMalformed)
    }

    func testPacketMessageDecodeSupportsAttachmentMeta4Compatibility() throws {
        let encoded = makePacketMessageData(attachmentMetaFieldCount: 4)
        guard let decoded = PacketRegistry.decode(from: encoded),
              let message = decoded.packet as? PacketMessage else {
            XCTFail("Failed to decode packet with 4 attachment meta fields")
            return
        }

        XCTAssertEqual(decoded.packetId, 0x06)
        XCTAssertFalse(message.isMalformed)
        XCTAssertEqual(message.fromPublicKey, "02from")
        XCTAssertEqual(message.toPublicKey, "02to")
        XCTAssertEqual(message.messageId, "msg-compat")
        XCTAssertEqual(message.aesChachaKey, "aes-key")
        XCTAssertEqual(message.attachments.count, 1)
        XCTAssertEqual(message.attachments[0].transportTag, "tag-1")
        XCTAssertEqual(message.attachments[0].transportServer, "cdn.rosetta.im")
    }

    func testPacketMessageDecodeSupportsAttachmentMeta0Compatibility() throws {
        let encoded = makePacketMessageData(attachmentMetaFieldCount: 0)
        guard let decoded = PacketRegistry.decode(from: encoded),
              let message = decoded.packet as? PacketMessage else {
            XCTFail("Failed to decode packet with 0 attachment meta fields")
            return
        }

        XCTAssertEqual(decoded.packetId, 0x06)
        XCTAssertFalse(message.isMalformed)
        XCTAssertEqual(message.messageId, "msg-compat")
        XCTAssertEqual(message.aesChachaKey, "aes-key")
        XCTAssertEqual(message.attachments.count, 1)
        XCTAssertEqual(message.attachments[0].transportTag, "")
        XCTAssertEqual(message.attachments[0].transportServer, "")
    }

    func testPacketMessageDecodeMarksMalformedForTruncatedOrMisalignedPayload() throws {
        let canonical = makePacketMessageData(attachmentMetaFieldCount: 2)
        let truncated = canonical.dropLast(3)
        let withTrailingByte = canonical + Data([0x00])

        guard let decodedTruncated = PacketRegistry.decode(from: Data(truncated)),
              let messageTruncated = decodedTruncated.packet as? PacketMessage else {
            XCTFail("Failed to decode truncated packet wrapper")
            return
        }
        XCTAssertTrue(messageTruncated.isMalformed)
        XCTAssertFalse(messageTruncated.malformedFingerprint.isEmpty)

        guard let decodedTrailing = PacketRegistry.decode(from: withTrailingByte),
              let messageTrailing = decodedTrailing.packet as? PacketMessage else {
            XCTFail("Failed to decode trailing-byte packet wrapper")
            return
        }
        XCTAssertTrue(messageTrailing.isMalformed)
        XCTAssertFalse(messageTrailing.malformedFingerprint.isEmpty)
    }

    func testSessionPacketContextResolverAcceptsGroupWireShape() throws {
        let own = "02my_account"

        var groupMessage = PacketMessage()
        groupMessage.fromPublicKey = "02group_member"
        groupMessage.toPublicKey = "#group:alpha"
        let messageContext = SessionManager.testResolveMessagePacketContext(groupMessage, ownKey: own)
        XCTAssertEqual(messageContext?.kind, "group")
        XCTAssertEqual(messageContext?.dialogKey, "#group:alpha")
        XCTAssertEqual(messageContext?.fromMe, false)

        var groupRead = PacketRead()
        groupRead.fromPublicKey = "02group_member"
        groupRead.toPublicKey = "#group:alpha"
        let readContext = SessionManager.testResolveReadPacketContext(groupRead, ownKey: own)
        XCTAssertEqual(readContext?.kind, "group")
        XCTAssertEqual(readContext?.dialogKey, "#group:alpha")
        XCTAssertEqual(readContext?.fromMe, false)

        var groupTyping = PacketTyping()
        groupTyping.fromPublicKey = "02group_member"
        groupTyping.toPublicKey = "#group:alpha"
        let typingContext = SessionManager.testResolveTypingPacketContext(groupTyping, ownKey: own)
        XCTAssertEqual(typingContext?.kind, "group")
        XCTAssertEqual(typingContext?.dialogKey, "#group:alpha")
        XCTAssertEqual(typingContext?.fromMe, false)
    }

    func testStreamEncodingSmoke() {
        let stream = Rosetta.Stream()
        _ = stream
        XCTAssertTrue(true)
    }

    private func makePacketMessageData(attachmentMetaFieldCount: Int) -> Data {
        let stream = Rosetta.Stream()
        stream.writeInt16(PacketMessage.packetId)
        stream.writeString("02from")
        stream.writeString("02to")
        stream.writeString("ciphertext")
        stream.writeString("chacha-key")
        stream.writeInt64(1_710_000_000_000)
        stream.writeString("hash")
        stream.writeString("msg-compat")
        stream.writeInt8(1)
        stream.writeString("att-1")
        stream.writeString("preview")
        stream.writeString("blob")
        stream.writeInt8(AttachmentType.image.rawValue)

        if attachmentMetaFieldCount >= 2 {
            stream.writeString("tag-1")
            stream.writeString("cdn.rosetta.im")
        }
        if attachmentMetaFieldCount >= 4 {
            stream.writeString("02encoded-for")
            stream.writeString("desktop")
        }

        stream.writeString("aes-key")
        return stream.toData()
    }
}
