import Foundation
import Testing
import NucleicProtocol

@testable import NucleicCore

private func mirrorEvent(_ seq: UInt64) -> AgentEvent {
    AgentEvent(
        sessionID: SessionID(rawValue: "s1"), seq: seq, at: Date(timeIntervalSince1970: 1_750_000_000),
        backend: .claudeCode, nativeType: nil,
        kind: .assistantText(TextChunk(messageID: "m\(seq)", text: "hi \(seq)", isPartial: false)))
}

private func mirrorHeader() -> SessionHeader {
    SessionHeader(
        sessionID: SessionID(rawValue: "s1"), backend: .claudeCode,
        worktree: "/tmp/wt", createdAt: Date(timeIntervalSince1970: 1_750_000_000))
}

private func tempRoot() -> URL {
    FileManager.default.temporaryDirectory.appendingPathComponent("nucleic-mirror-\(UUID().uuidString)")
}

@Suite struct TranscriptMirrorStoreTests {
    @Test func writeFullPreservesOriginalSeqs() async throws {
        let root = tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let store = TranscriptMirrorStore(root: root)
        let sid = SessionID(rawValue: "s1")

        let held = try await store.writeFull(
            sid, sourceDeviceID: "devA", header: mirrorHeader(),
            events: (1...5).map { mirrorEvent(UInt64($0)) })
        #expect(held == 5)
        #expect(await store.heldSeq(sid) == 5)
        #expect(await store.sourceDeviceID(sid) == "devA")

        // The mirror must be a verbatim copy — seqs NOT re-stamped by the writer.
        let url = try #require(await store.url(sid))
        let read = try TranscriptReader(url: url).read()
        #expect(read.events.map(\.seq) == [1, 2, 3, 4, 5])
        #expect(read.header.sessionID == sid)
    }

    @Test func appendExtendsContiguousAndStopsAtGap() async throws {
        let root = tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let store = TranscriptMirrorStore(root: root)
        let sid = SessionID(rawValue: "s1")
        _ = try await store.writeFull(
            sid, sourceDeviceID: "devA", header: mirrorHeader(),
            events: (1...3).map { mirrorEvent(UInt64($0)) })

        // 4,5 are contiguous; 7,8 are past a gap and must be dropped (not committed as complete).
        let held = try await store.append(sid, events: [4, 5, 7, 8].map { mirrorEvent($0) })
        #expect(held == 5)
        #expect(await store.heldSeq(sid) == 5)
        let url = try #require(await store.url(sid))
        #expect(try TranscriptReader(url: url).read().events.map(\.seq) == [1, 2, 3, 4, 5])
    }

    @Test func heldSeqRecoveredByScanningFreshStore() async throws {
        let root = tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let sid = SessionID(rawValue: "s1")
        _ = try await TranscriptMirrorStore(root: root).writeFull(
            sid, sourceDeviceID: "devA", header: mirrorHeader(),
            events: (1...7).map { mirrorEvent(UInt64($0)) })

        // A brand-new store (cold index) must recover the held cursor from disk.
        let fresh = TranscriptMirrorStore(root: root)
        #expect(await fresh.heldSeq(sid) == 7)
        #expect(await fresh.has(sid))
    }

    @Test func removeSourceDropsThatDevicesMirrors() async throws {
        let root = tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let store = TranscriptMirrorStore(root: root)
        _ = try await store.writeFull(
            SessionID(rawValue: "s1"), sourceDeviceID: "devA", header: mirrorHeader(),
            events: [mirrorEvent(1)])
        #expect(await store.has(SessionID(rawValue: "s1")))

        await store.removeSource("devA")
        #expect(await store.has(SessionID(rawValue: "s1")) == false)
        #expect(await store.heldSeq(SessionID(rawValue: "s1")) == 0)
    }
}

@Suite struct TranscriptSyncCoordinatorTests {
    /// Records which (session, afterSeq, holder) fetches the coordinator issued.
    private actor CallLog {
        private(set) var calls: [(sessionID: SessionID, afterSeq: UInt64, holder: String)] = []
        func record(_ s: SessionID, _ a: UInt64, _ h: String) { calls.append((s, a, h)) }
        var count: Int { calls.count }
    }

    /// A fetch closure that serves a canned gap-fill from any holder in `goodHolders` and replies
    /// `notHeld` from everyone else, logging every call.
    private func makeFetch(log: CallLog, goodHolders: Set<String>, tip: UInt64)
        -> @Sendable (SessionID, UInt64, String) async -> AsyncStream<PeerClient.TranscriptFetchUpdate> {
        { sessionID, afterSeq, holder in
            await log.record(sessionID, afterSeq, holder)
            return AsyncStream { cont in
                if goodHolders.contains(holder), afterSeq < tip {
                    let events = ((afterSeq + 1)...tip).map { mirrorEvent($0) }
                    cont.yield(.chunk(TranscriptChunk(
                        requestID: "x", sessionID: sessionID, batchIndex: 0, events: events)))
                    cont.yield(.complete(TranscriptFetchComplete(
                        requestID: "x", sessionID: sessionID, throughSeq: tip,
                        headerJSON: afterSeq == 0 ? (try? mirrorHeader().encodedForWire()) : nil)))
                } else if goodHolders.contains(holder) {
                    cont.yield(.complete(TranscriptFetchComplete(
                        requestID: "x", sessionID: sessionID, throughSeq: afterSeq)))
                } else {
                    cont.yield(.unavailable(TranscriptUnavailable(
                        requestID: "x", sessionID: sessionID, reason: .notHeld)))
                }
                cont.finish()
            }
        }
    }

    private func settle(_ coordinator: TranscriptSyncCoordinator, _ sid: SessionID) async {
        var n = 0
        while await coordinator.isFetching(sid), n < 10_000 { await Task.yield(); n += 1 }
    }

    @Test func backfillsGapFromOwner() async throws {
        let root = tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let store = TranscriptMirrorStore(root: root)
        let log = CallLog()
        let coordinator = TranscriptSyncCoordinator(
            mirrorStore: store, fetch: makeFetch(log: log, goodHolders: ["owner"], tip: 5))
        let sid = SessionID(rawValue: "s1")

        await coordinator.sync(
            sessions: [.init(sessionID: sid, lastSeq: 5, archived: false, ownerDeviceID: "owner")],
            candidates: ["owner"])
        await settle(coordinator, sid)

        #expect(await store.heldSeq(sid) == 5)
        let url = try #require(await store.url(sid))
        #expect(try TranscriptReader(url: url).read().events.map(\.seq) == [1, 2, 3, 4, 5])
    }

    @Test func fallsThroughNotHeldToNextHolder() async throws {
        let root = tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let store = TranscriptMirrorStore(root: root)
        // The owner is offline (not a candidate); peerB doesn't hold it, peerC does.
        let log = CallLog()
        let coordinator = TranscriptSyncCoordinator(
            mirrorStore: store, fetch: makeFetch(log: log, goodHolders: ["peerC"], tip: 4))
        let sid = SessionID(rawValue: "s1")

        await coordinator.sync(
            sessions: [.init(sessionID: sid, lastSeq: 4, archived: false, ownerDeviceID: "owner")],
            candidates: ["peerB", "peerC"])
        await settle(coordinator, sid)

        #expect(await store.heldSeq(sid) == 4)
        #expect(await log.count == 2)  // probed peerB (notHeld) then peerC (served)
    }

    @Test func noFetchWhenAlreadyCurrent() async throws {
        let root = tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let store = TranscriptMirrorStore(root: root)
        let sid = SessionID(rawValue: "s1")
        _ = try await store.writeFull(
            sid, sourceDeviceID: "owner", header: mirrorHeader(),
            events: (1...5).map { mirrorEvent(UInt64($0)) })
        let log = CallLog()
        let coordinator = TranscriptSyncCoordinator(
            mirrorStore: store, fetch: makeFetch(log: log, goodHolders: ["owner"], tip: 5))

        await coordinator.sync(
            sessions: [.init(sessionID: sid, lastSeq: 5, archived: false, ownerDeviceID: "owner")],
            candidates: ["owner"])
        await settle(coordinator, sid)

        #expect(await log.count == 0)  // held == tip ⇒ no gap ⇒ no fetch
    }

    @Test func archivedSessionDropsMirror() async throws {
        let root = tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let store = TranscriptMirrorStore(root: root)
        let sid = SessionID(rawValue: "s1")
        _ = try await store.writeFull(
            sid, sourceDeviceID: "owner", header: mirrorHeader(), events: [mirrorEvent(1)])
        let log = CallLog()
        let coordinator = TranscriptSyncCoordinator(
            mirrorStore: store, fetch: makeFetch(log: log, goodHolders: ["owner"], tip: 1))

        await coordinator.sync(
            sessions: [.init(sessionID: sid, lastSeq: 1, archived: true, ownerDeviceID: "owner")],
            candidates: ["owner"])

        #expect(await store.has(sid) == false)  // archived ⇒ mirror dropped
    }
}
