import Testing
import Foundation
import Network
@testable import Music

@MainActor
struct HostServerIntegrationTests {

    // Starts a HostServer, connects via TCP, sends GET /db,
    // verifies the response contains valid SQLite data.
    @Test(.timeLimit(.minutes(1)))
    func dbDownloadReturnsValidSQLite() async throws {
        // 1. Create a temp directory and a SQLite database with one track
        let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
        let dbPath = tempDir.appendingPathComponent("db.sqlite").path
        let db = try DatabaseService(path: dbPath)
        var track = Track.fixture(fileURL: "/test.mp3")
        try db.insert(&track)

        // 2. Start the HostServer (configured with db for WAL checkpoint) and wait for the listener to be ready
        let server = HostServer(dbPath: dbPath)
        server.configure(player: nil, db: db)
        try server.start()
        try await Task.sleep(for: .milliseconds(200))
        let port = server.actualPort!

        // 3. Perform an HTTP GET /db request to download the database
        let responseData = try await httpGet(host: "127.0.0.1", port: port, path: "/db")

        // 4. Verify the response starts with the SQLite magic header
        let header = String(data: responseData.prefix(16), encoding: .utf8) ?? ""
        #expect(header.hasPrefix("SQLite format 3"))

        // 5. Write the downloaded data to disk and verify it contains the inserted track
        let downloadedPath = tempDir.appendingPathComponent("downloaded.sqlite").path
        try responseData.write(to: URL(fileURLWithPath: downloadedPath))
        let downloadedDb = try DatabaseService(path: downloadedPath)
        #expect(try downloadedDb.trackCount() == 1)

        // 6. Clean up
        server.stop()
        try? FileManager.default.removeItem(at: tempDir)
    }

    // Connects to /cmd, sends a pause command, verifies a playbackState event comes back.
    @Test(.timeLimit(.minutes(1)))
    func commandChannelRoundTrip() async throws {
        // 1. Create a temp directory and an empty SQLite database
        let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
        let dbPath = tempDir.appendingPathComponent("db.sqlite").path
        _ = try DatabaseService(path: dbPath)

        // 2. Set up the player and server with command dispatch configured
        let audio = AudioService()
        let player = PlayerViewModel(audio: audio, db: nil)
        let server = HostServer(dbPath: dbPath)
        server.configure(player: player, db: nil)
        try server.start()
        try await Task.sleep(for: .milliseconds(200))
        let port = server.actualPort!

        // 3. Open a command channel connection via GET /cmd
        let connection = try await connectCommandChannel(host: "127.0.0.1", port: port)

        // 4. Send a pause command as NDJSON
        let pauseCmd = try JSONEncoder().encode(RemoteCommand.pause)
        var lineData = pauseCmd
        lineData.append(contentsOf: "\n".utf8)
        connection.send(content: lineData, completion: .contentProcessed { _ in })

        // 5. Wait for and decode the playbackState response event
        let responseLine = try await receiveOneLine(on: connection)
        let event = try JSONDecoder().decode(HostEvent.self, from: Data(responseLine.utf8))

        // 6. Verify it is a playbackState event with isPlaying == false
        if case .playbackState(let payload) = event {
            #expect(payload.isPlaying == false)
        } else {
            Issue.record("Expected playbackState, got \(event)")
        }

        // 7. Clean up
        connection.cancel()
        server.stop()
        try? FileManager.default.removeItem(at: tempDir)
    }

    // MARK: - Helpers

    /// Performs a simple HTTP GET using NWConnection and returns the response body.
    private func httpGet(host: String, port: UInt16, path: String) async throws -> Data {
        try await withCheckedThrowingContinuation { continuation in
            let connection = NWConnection(
                host: NWEndpoint.Host(host),
                port: NWEndpoint.Port(rawValue: port)!,
                using: .tcp
            )
            connection.stateUpdateHandler = { state in
                if case .ready = state {
                    // Send the HTTP request
                    let request = "GET \(path) HTTP/1.1\r\nHost: \(host)\r\nConnection: close\r\n\r\n"
                    connection.send(content: Data(request.utf8), completion: .contentProcessed { _ in })
                    // Receive the full response (Connection: close ensures we get everything)
                    connection.receiveMessage { data, _, _, error in
                        if let error {
                            continuation.resume(throwing: error)
                        } else if let data, let range = data.range(of: Data("\r\n\r\n".utf8)) {
                            // Strip HTTP headers, return just the body
                            continuation.resume(returning: Data(data[range.upperBound...]))
                        } else {
                            continuation.resume(returning: data ?? Data())
                        }
                        connection.cancel()
                    }
                } else if case .failed(let error) = state {
                    continuation.resume(throwing: error)
                }
            }
            connection.start(queue: .main)
        }
    }

    /// Opens a TCP connection to the /cmd endpoint and waits for the HTTP response header.
    private func connectCommandChannel(host: String, port: UInt16) async throws -> NWConnection {
        try await withCheckedThrowingContinuation { continuation in
            let connection = NWConnection(
                host: NWEndpoint.Host(host),
                port: NWEndpoint.Port(rawValue: port)!,
                using: .tcp
            )
            connection.stateUpdateHandler = { state in
                if case .ready = state {
                    // Send the HTTP upgrade request for the command channel
                    let request = "GET /cmd HTTP/1.1\r\nHost: \(host)\r\nConnection: keep-alive\r\n\r\n"
                    connection.send(content: Data(request.utf8), completion: .contentProcessed { _ in })
                    // Consume the HTTP 200 response header
                    connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { _, _, _, _ in
                        continuation.resume(returning: connection)
                    }
                } else if case .failed(let error) = state {
                    continuation.resume(throwing: error)
                }
            }
            connection.start(queue: .main)
        }
    }

    /// Reads one newline-delimited line from a connection.
    private func receiveOneLine(on connection: NWConnection) async throws -> String {
        try await withCheckedThrowingContinuation { continuation in
            connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, _, error in
                if let error {
                    continuation.resume(throwing: error)
                } else {
                    let text = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
                    // Extract the first complete line from the received data
                    continuation.resume(returning: text.split(separator: "\n").first.map(String.init) ?? text)
                }
            }
        }
    }
}
