import Foundation
import SQLite3
import XCTest
@testable import Rosetta

struct SchemaSnapshot {
    let tables: Set<String>
    let columnsByTable: [String: Set<String>]
    let indexes: Set<String>
}

struct NormalizedDbSnapshot: Equatable {
    struct Message: Equatable {
        let account: String
        let dialogKey: String
        let messageId: String
        let fromMe: Bool
        let read: Bool
        let delivered: Int
        let plainMessage: String
        let timestamp: Int64
        let hasAttachments: Bool
    }

    struct Dialog: Equatable {
        let opponentKey: String
        let lastMessage: String
        let lastMessageTimestamp: Int64
        let unreadCount: Int
        let iHaveSent: Bool
        let isRequest: Bool
        let lastMessageFromMe: Bool
        let lastMessageDelivered: Int
        let lastMessageRead: Bool
    }

    let messages: [Message]
    let dialogs: [Dialog]
    let syncCursor: Int64
}

enum FixtureEvent {
    case incoming(opponent: String, messageId: String, timestamp: Int64, text: String, attachments: [MessageAttachment] = [])
    case incomingPacket(from: String, to: String, messageId: String, timestamp: Int64, text: String, attachments: [MessageAttachment] = [])
    case outgoing(opponent: String, messageId: String, timestamp: Int64, text: String, attachments: [MessageAttachment] = [])
    case markDelivered(opponent: String, messageId: String)
    case markOutgoingRead(opponent: String)
    case applyReadPacket(from: String, to: String)
    case saveSyncCursor(Int64)
}

struct FixtureScenario {
    let name: String
    let events: [FixtureEvent]
}

final class SQLiteTestDB {
    private var handle: OpaquePointer?

    init(path: String) throws {
        if sqlite3_open(path, &handle) != SQLITE_OK {
            let message = String(cString: sqlite3_errmsg(handle))
            sqlite3_close(handle)
            throw NSError(domain: "SQLiteTestDB", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
        }
    }

    deinit {
        sqlite3_close(handle)
    }

    func execute(_ sql: String) throws {
        var errorMessage: UnsafeMutablePointer<Int8>?
        if sqlite3_exec(handle, sql, nil, nil, &errorMessage) != SQLITE_OK {
            let message = errorMessage.map { String(cString: $0) } ?? "Unknown sqlite error"
            sqlite3_free(errorMessage)
            throw NSError(domain: "SQLiteTestDB", code: 2, userInfo: [NSLocalizedDescriptionKey: message, "sql": sql])
        }
    }

    func query(_ sql: String, _ bindings: [Binding] = []) throws -> [[String: String]] {
        var statement: OpaquePointer?
        guard sqlite3_prepare_v2(handle, sql, -1, &statement, nil) == SQLITE_OK else {
            let message = String(cString: sqlite3_errmsg(handle))
            throw NSError(domain: "SQLiteTestDB", code: 3, userInfo: [NSLocalizedDescriptionKey: message, "sql": sql])
        }
        defer { sqlite3_finalize(statement) }

        for (index, binding) in bindings.enumerated() {
            let idx = Int32(index + 1)
            switch binding {
            case .text(let value):
                sqlite3_bind_text(statement, idx, value, -1, SQLITE_TRANSIENT)
            case .int64(let value):
                sqlite3_bind_int64(statement, idx, value)
            }
        }

        var rows: [[String: String]] = []
        while sqlite3_step(statement) == SQLITE_ROW {
            let columnCount = sqlite3_column_count(statement)
            var row: [String: String] = [:]
            for column in 0..<columnCount {
                let name = String(cString: sqlite3_column_name(statement, column))
                if let valuePtr = sqlite3_column_text(statement, column) {
                    row[name] = String(cString: valuePtr)
                } else {
                    row[name] = ""
                }
            }
            rows.append(row)
        }
        return rows
    }

    enum Binding {
        case text(String)
        case int64(Int64)
    }
}

private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)

@MainActor
final class DBTestContext {
    let account: String
    let storagePassword = "test-storage-password"

    init(account: String = "02_ios_sql_test_\(UUID().uuidString.replacingOccurrences(of: "-", with: ""))") {
        self.account = account
    }

    var databaseURL: URL {
        DatabaseManager.databaseURLForTesting(accountPublicKey: account)
    }

    func bootstrap() async throws {
        try DatabaseManager.shared.bootstrap(accountPublicKey: account)
        await MessageRepository.shared.bootstrap(accountPublicKey: account, storagePassword: storagePassword)
        await DialogRepository.shared.bootstrap(accountPublicKey: account, storagePassword: storagePassword)
    }

    func teardown() {
        MessageRepository.shared.reset()
        DialogRepository.shared.reset()
        DatabaseManager.shared.close()
        DatabaseManager.shared.deleteDatabase(for: account)
    }

    func openSQLite() throws -> SQLiteTestDB {
        try SQLiteTestDB(path: databaseURL.path)
    }

    func schemaSnapshot() throws -> SchemaSnapshot {
        let sqlite = try openSQLite()
        let tableRows = try sqlite.query("SELECT name FROM sqlite_master WHERE type='table'")
        let tables = Set(tableRows.compactMap { $0["name"] })

        var columnsByTable: [String: Set<String>] = [:]
        for table in tables {
            let rows = try sqlite.query("PRAGMA table_info('\(table)')")
            columnsByTable[table] = Set(rows.compactMap { $0["name"] })
        }

        let indexRows = try sqlite.query("SELECT name FROM sqlite_master WHERE type='index'")
        let indexes = Set(indexRows.compactMap { $0["name"] })

        return SchemaSnapshot(tables: tables, columnsByTable: columnsByTable, indexes: indexes)
    }

    func normalizedSnapshot() throws -> NormalizedDbSnapshot {
        let sqlite = try openSQLite()

        let messageRows = try sqlite.query(
            """
            SELECT account, dialog_key, message_id, from_me, is_read, delivery_status,
                   COALESCE(NULLIF(plain_message, ''), text) AS plain_message,
                   timestamp, attachments
            FROM messages
            WHERE account = ?
            ORDER BY dialog_key, timestamp, message_id
            """,
            [.text(account)]
        )

        let messages = messageRows.map { row in
            NormalizedDbSnapshot.Message(
                account: row["account", default: ""],
                dialogKey: row["dialog_key", default: ""],
                messageId: row["message_id", default: ""],
                fromMe: row["from_me"] == "1",
                read: row["is_read"] == "1",
                delivered: Int(row["delivery_status", default: "0"]) ?? 0,
                plainMessage: row["plain_message", default: ""],
                timestamp: Int64(row["timestamp", default: "0"]) ?? 0,
                hasAttachments: row["attachments", default: "[]"] != "[]"
            )
        }

        let dialogRows = try sqlite.query(
            """
            SELECT opponent_key, last_message, last_message_timestamp, unread_count,
                   i_have_sent, is_request, last_message_from_me,
                   last_message_delivered, last_message_read
            FROM dialogs
            WHERE account = ?
            ORDER BY opponent_key
            """,
            [.text(account)]
        )

        let dialogs = dialogRows.map { row in
            NormalizedDbSnapshot.Dialog(
                opponentKey: row["opponent_key", default: ""],
                lastMessage: row["last_message", default: ""],
                lastMessageTimestamp: Int64(row["last_message_timestamp", default: "0"]) ?? 0,
                unreadCount: Int(row["unread_count", default: "0"]) ?? 0,
                iHaveSent: row["i_have_sent"] == "1",
                isRequest: row["is_request"] == "1",
                lastMessageFromMe: row["last_message_from_me"] == "1",
                lastMessageDelivered: Int(row["last_message_delivered", default: "0"]) ?? 0,
                lastMessageRead: row["last_message_read"] == "1"
            )
        }

        let cursorRow = try sqlite.query(
            "SELECT last_sync FROM accounts_sync_times WHERE account = ? LIMIT 1",
            [.text(account)]
        ).first
        let syncCursor = Int64(cursorRow?["last_sync"] ?? "0") ?? 0

        return NormalizedDbSnapshot(messages: messages, dialogs: dialogs, syncCursor: syncCursor)
    }

    func runScenario(_ scenario: FixtureScenario) async throws {
        func normalizeGroupDialogKey(_ value: String) -> String {
            let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
            let lower = trimmed.lowercased()
            if lower.hasPrefix("group:") {
                let id = String(trimmed.dropFirst("group:".count)).trimmingCharacters(in: .whitespacesAndNewlines)
                return id.isEmpty ? trimmed : "#group:\(id)"
            }
            return trimmed
        }

        func resolveDialogIdentity(from: String, to: String) -> String? {
            let fromKey = from.trimmingCharacters(in: .whitespacesAndNewlines)
            let toKey = to.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !fromKey.isEmpty, !toKey.isEmpty else { return nil }
            if DatabaseManager.isGroupDialogKey(toKey) {
                return normalizeGroupDialogKey(toKey)
            }
            if fromKey == account { return toKey }
            if toKey == account { return fromKey }
            return nil
        }

        for event in scenario.events {
            switch event {
            case .incoming(let opponent, let messageId, let timestamp, let text, let attachments):
                var packet = PacketMessage()
                packet.fromPublicKey = opponent
                packet.toPublicKey = account
                packet.messageId = messageId
                packet.timestamp = timestamp
                packet.attachments = attachments
                MessageRepository.shared.upsertFromMessagePacket(packet, myPublicKey: account, decryptedText: text)
                DialogRepository.shared.updateDialogFromMessages(opponentKey: opponent)

            case .incomingPacket(let from, let to, let messageId, let timestamp, let text, let attachments):
                guard let dialogIdentity = resolveDialogIdentity(from: from, to: to) else { continue }
                var packet = PacketMessage()
                packet.fromPublicKey = from
                packet.toPublicKey = to
                packet.messageId = messageId
                packet.timestamp = timestamp
                packet.attachments = attachments
                MessageRepository.shared.upsertFromMessagePacket(
                    packet,
                    myPublicKey: account,
                    decryptedText: text,
                    dialogIdentityOverride: dialogIdentity
                )
                DialogRepository.shared.updateDialogFromMessages(opponentKey: dialogIdentity)

            case .outgoing(let opponent, let messageId, let timestamp, let text, let attachments):
                var packet = PacketMessage()
                packet.fromPublicKey = account
                packet.toPublicKey = opponent
                packet.messageId = messageId
                packet.timestamp = timestamp
                packet.attachments = attachments
                MessageRepository.shared.upsertFromMessagePacket(packet, myPublicKey: account, decryptedText: text)
                DialogRepository.shared.updateDialogFromMessages(opponentKey: opponent)

            case .markDelivered(let opponent, let messageId):
                MessageRepository.shared.updateDeliveryStatus(messageId: messageId, status: .delivered)
                DialogRepository.shared.updateDialogFromMessages(opponentKey: opponent)

            case .markOutgoingRead(let opponent):
                MessageRepository.shared.markOutgoingAsRead(opponentKey: opponent, myPublicKey: account)
                DialogRepository.shared.updateDialogFromMessages(opponentKey: opponent)

            case .applyReadPacket(let from, let to):
                let fromKey = from.trimmingCharacters(in: .whitespacesAndNewlines)
                let toKey = to.trimmingCharacters(in: .whitespacesAndNewlines)
                guard !fromKey.isEmpty, !toKey.isEmpty else { continue }

                if DatabaseManager.isGroupDialogKey(toKey) {
                    let dialogIdentity = normalizeGroupDialogKey(toKey)
                    if fromKey == account {
                        MessageRepository.shared.markIncomingAsRead(opponentKey: dialogIdentity, myPublicKey: account)
                        DialogRepository.shared.markAsRead(opponentKey: dialogIdentity)
                    } else {
                        MessageRepository.shared.markOutgoingAsRead(opponentKey: dialogIdentity, myPublicKey: account)
                        DialogRepository.shared.markOutgoingAsRead(opponentKey: dialogIdentity)
                    }
                    continue
                }

                if fromKey == account {
                    MessageRepository.shared.markIncomingAsRead(opponentKey: toKey, myPublicKey: account)
                    DialogRepository.shared.markAsRead(opponentKey: toKey)
                } else if toKey == account {
                    MessageRepository.shared.markOutgoingAsRead(opponentKey: fromKey, myPublicKey: account)
                    DialogRepository.shared.markOutgoingAsRead(opponentKey: fromKey)
                }

            case .saveSyncCursor(let timestamp):
                DatabaseManager.shared.saveSyncCursor(account: account, timestamp: timestamp)
            }
        }
    }
}
