import Foundation
import Testing

@testable import NucleicCore
import NucleicProtocol

/// CARBON_SHARDING §17.9 (fence + authority half): the landing host releases immediately; a
/// member without the landed content keeps seeing the lock held (`releasing — syncing
/// files`) until manifests + shards are held AND packs applied/refs fast-forwarded; two
/// contenders are never both granted; a diverged local branch never force-updates.
@Suite("Carbon lock release fence", .serialized)
struct CarbonLockFenceTests {
    private let projectUUID = UUID(uuidString: "5C6D7E8F-9A0B-4123-8456-0123456789AB")!
    private let key = try! CarbonCrypto.ProjectKey(bytes: Data((0..<32).map { UInt8($0 &+ 41) }))
    private let dev = LockDomain("dev")
    private var project: CarbonManifest.ProjectRef { .init(uuid: projectUUID) }

    private func sid(_ s: String) -> SessionID { SessionID(rawValue: s) }

    private func tempRoot() throws -> URL {
        let url = FileManager.default.temporaryDirectory
            .appendingPathComponent("carbon-lock-fence-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
        return url
    }

    private func makeStore(root: URL, name: String) -> CarbonShardStore {
        CarbonShardStore(
            root: root.appendingPathComponent(name, isDirectory: true),
            keyProvider: { [key] _ in key })
    }

    /// Write one synthetic history manifest (single shard) into `store` and return its head.
    @discardableResult
    private func produceHistory(
        on store: CarbonShardStore, seq: UInt64, parent: String? = nil, payload: String? = nil
    ) async throws -> (head: CarbonHead, manifestID: String) {
        let crypto = CarbonCrypto(projectKey: key)
        let sealed = try crypto.seal(Data((payload ?? "hist-\(seq)").utf8))
        try await store.putShard(
            ciphertext: sealed.ciphertext, expectedID: sealed.id, projectUUID: projectUUID)
        let manifest = CarbonManifest(
            project: project, stream: .history, sessionID: nil, ownerDeviceID: "owner",
            ownerGeneration: 1, snapshotSeq: seq, parentManifest: parent, payloadKind: .gitPack,
            shards: [.init(id: sealed.id, byteCount: UInt64(sealed.ciphertext.count))],
            createdAtMs: seq)
        let sealedManifest = try manifest.sealed(with: crypto)
        try await store.putManifest(
            ciphertext: sealedManifest.ciphertext, expectedID: sealedManifest.id,
            projectUUID: projectUUID)
        let head = manifest.head(manifestID: sealedManifest.id)
        try await store.advanceHead(head)
        return (head, sealedManifest.id)
    }

    // MARK: - Landing host (degenerate case: zero added latency)

    /// The landing host holds `requiredHeads` by construction, so a fenced release frees the
    /// path immediately — a waiter is granted with no extra round anywhere (§17.4).
    @Test func landingHostReleasesImmediately() async throws {
        let root = try tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let store = makeStore(root: root, name: "owner")
        let produced = try await produceHistory(on: store, seq: 1)

        let lm = CarbonLockManager()
        let fence = CarbonLockFence(
            store: store, keyProvider: { [key] _ in key }, repoPath: { _ in nil })
        await lm.setFenceCheck { await fence.satisfied($0) }

        #expect(await lm.acquire(sid("A"), domain: dev, files: ["x"]) == .granted)
        let waiter = Task { await lm.acquire(sid("B"), domain: dev, files: ["x"]) }
        while await !lm.isWaiting(sid("B")) { try? await Task.sleep(for: .milliseconds(5)) }
        await lm.release(sid("A"), paths: ["x"], requiredHeads: [produced.head])
        #expect(await waiter.value == .granted)      // no releasing limbo on the landing host
        #expect(await lm.releasingPaths(in: dev).isEmpty)
    }

    /// A member that does NOT hold the landed content keeps the path blocked
    /// (`releasing — syncing files`); it frees only once the chain syncs (§17.4 stage 2).
    @Test func strugglerKeepsSeeingHeldUntilContentArrives() async throws {
        let root = try tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let ownerStore = makeStore(root: root, name: "owner")
        let memberStore = makeStore(root: root, name: "member")
        let produced = try await produceHistory(on: ownerStore, seq: 1)

        // The member's lock manager (folded view driver) with its own store-backed fence.
        let lm = CarbonLockManager()
        let fence = CarbonLockFence(
            store: memberStore, keyProvider: { [key] _ in key }, repoPath: { _ in nil })
        await lm.setFenceCheck { await fence.satisfied($0) }

        #expect(await lm.acquire(sid("A"), domain: dev, files: ["x"]) == .granted)
        await lm.release(sid("A"), paths: ["x"], requiredHeads: [produced.head])

        // Published but not observed: the path stays blocked, acquires queue.
        #expect(await lm.releasingPaths(in: dev) == ["x"])
        let waiter = Task { await lm.acquire(sid("B"), domain: dev, files: ["x"]) }
        while await !lm.isWaiting(sid("B")) { try? await Task.sleep(for: .milliseconds(5)) }
        #expect(await lm.isWaiting(sid("B")))

        // The content arrives (chain sync) ⇒ recheck clears the fence and grants the waiter.
        let mesh = MeshStores(["owner": ownerStore])
        let coordinator = CarbonSyncCoordinator(
            store: memberStore, keyProvider: { [key] _ in key }, transport: mesh.transport())
        await coordinator.observeHeadAdvanced(produced.head, from: "owner")
        await lm.recheckFences()
        #expect(await waiter.value == .granted)
        #expect(await lm.releasingPaths(in: dev).isEmpty)
    }

    /// The ledger publication rides the manager's mutations in order; the fold shows the
    /// releasing fence while unobserved and the new holder after re-acquisition — and two
    /// contenders are never both holders (§17.9 loopback assertion, component form).
    @Test func managerPublishesLedgerAndNeverDoubleGrants() async throws {
        let root = try tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let store = makeStore(root: root, name: "owner")
        let ledger = CarbonLockLedger(store: store, keyProvider: { [key] _ in key })
        let produced = try await produceHistory(on: store, seq: 1)

        let lm = CarbonLockManager()
        let projectRef = project
        await lm.setLedgerAppend { record in
            _ = try? await ledger.append(
                CarbonLockEvent(
                    kind: record.kind, domainRef: record.domain.ref,
                    sessionID: record.session?.rawValue, deviceID: "owner",
                    paths: record.paths,
                    requiredHeads: record.requiredHeads.isEmpty ? nil : record.requiredHeads),
                project: projectRef, ownerDeviceID: "owner", generation: 1)
        }

        #expect(await lm.acquire(sid("A"), domain: dev, files: ["x"]) == .granted)
        let waiter = Task { await lm.acquire(sid("B"), domain: dev, files: ["x"]) }
        while await !lm.isWaiting(sid("B")) { try? await Task.sleep(for: .milliseconds(5)) }
        // Contention invariant: while A holds, B is queued — never both holders.
        let mid = await lm.state(in: dev)
        #expect(mid.holders.map(\.session) == [sid("A")])
        #expect(mid.waiters.map(\.session) == [sid("B")])

        await lm.release(sid("A"), paths: ["x"], requiredHeads: [produced.head])
        #expect(await waiter.value == .granted)
        await lm.ledgerQuiesce()

        let fold = try await ledger.fold(projectUUID: projectUUID)
        let devFold = fold.domains["dev"]
        #expect(devFold?.held["x"]?.sessionID == "B")     // re-acquire superseded the fence
        #expect(devFold?.releasing.isEmpty == true)
        #expect(devFold?.waiters.isEmpty == true)
    }

    // MARK: - Packs applied + refs fast-forwarded (real git)

    /// End-to-end §17.4 stage 2 on a real repo pair: the owner lands a commit on `dev` and
    /// produces the base-history manifest; a member holding only the older clone satisfies
    /// the fence only after the fence itself applies the pack and fast-forwards its local
    /// `dev` — and a diverged local `dev` is never force-updated.
    @Test func fenceAppliesPacksAndFastForwardsLocalBranch() async throws {
        let root = try tempRoot()
        defer { try? FileManager.default.removeItem(at: root) }
        let git = GitRunner()

        // Owner repo: dev @ c1 → c2.
        let ownerRepo = root.appendingPathComponent("owner-repo").path
        try FileManager.default.createDirectory(
            at: URL(fileURLWithPath: ownerRepo), withIntermediateDirectories: true)
        try await git.checked(["init", "-b", "dev"], in: ownerRepo)
        try await git.checked(["config", "user.name", "Fixture"], in: ownerRepo)
        try await git.checked(["config", "user.email", "fixture@test.local"], in: ownerRepo)
        try Data("one\n".utf8).write(to: URL(fileURLWithPath: ownerRepo).appendingPathComponent("a.txt"))
        try await git.checked(["add", "-A"], in: ownerRepo)
        try await git.checked(["commit", "-m", "c1"], in: ownerRepo)
        let c1 = try await git.checked(["rev-parse", "dev"], in: ownerRepo)

        // Member clone at c1 (the straggler's local state). `clone` does not carry the source's
        // LOCAL config, so the identity has to be set again here — this repo commits below
        // ("local divergence"), and without it the commit fails on any machine with no global
        // git identity (a fresh CI runner, or a dev box that has never set one).
        let memberRepo = root.appendingPathComponent("member-repo").path
        try await git.checked(["clone", "--branch", "dev", ownerRepo, memberRepo], in: root.path)
        try await git.checked(["config", "user.name", "Fixture"], in: memberRepo)
        try await git.checked(["config", "user.email", "fixture@test.local"], in: memberRepo)

        // Owner lands c2 and produces the base-history manifest at the landing site (§6.6).
        try Data("two\n".utf8).write(to: URL(fileURLWithPath: ownerRepo).appendingPathComponent("b.txt"))
        try await git.checked(["add", "-A"], in: ownerRepo)
        try await git.checked(["commit", "-m", "c2"], in: ownerRepo)
        let c2 = try await git.checked(["rev-parse", "dev"], in: ownerRepo)

        let ownerStore = makeStore(root: root, name: "owner-store")
        let snapshotter = CarbonSnapshotter(
            git: git, store: ownerStore, keyProvider: { [key] _ in key })
        _ = try await snapshotter.snapshotHistory(
            project: project, sessionID: nil, repoPath: ownerRepo, ref: "refs/heads/dev",
            originSafeSHA: c1, ownerDeviceID: "owner", ownerGeneration: 1)
        let chainKey = CarbonStreamKey(projectUUID: projectUUID, stream: .history, sessionID: nil)
        let head = try #require(await ownerStore.heads(projectUUID: projectUUID)[chainKey])

        // Member fence: fails while the chain isn't held locally…
        let memberStore = makeStore(root: root, name: "member-store")
        let fence = CarbonLockFence(
            git: git, store: memberStore, keyProvider: { [key] _ in key },
            repoPath: { [projectUUID] in $0 == projectUUID ? memberRepo : nil })
        #expect(await fence.satisfied([head]) == false)

        // …then, once the chain syncs, it applies the pack and fast-forwards local `dev`.
        let mesh = MeshStores(["owner": ownerStore])
        let coordinator = CarbonSyncCoordinator(
            store: memberStore, keyProvider: { [key] _ in key }, transport: mesh.transport())
        await coordinator.observeHeadAdvanced(head, from: "owner")
        #expect(await fence.satisfied([head]) == true)
        let memberDev = try await git.checked(["rev-parse", "refs/heads/dev"], in: memberRepo)
        #expect(memberDev == c2)

        // Diverged local branch (§17.8 non-FF row): never force — the fence fails instead.
        try await git.checked(["reset", "--hard", c1], in: memberRepo)
        try Data("theirs\n".utf8).write(
            to: URL(fileURLWithPath: memberRepo).appendingPathComponent("c.txt"))
        try await git.checked(["add", "-A"], in: memberRepo)
        try await git.checked(["commit", "-m", "local divergence"], in: memberRepo)
        let diverged = try await git.checked(["rev-parse", "refs/heads/dev"], in: memberRepo)
        #expect(await fence.satisfied([head]) == false)
        let after = try await git.checked(["rev-parse", "refs/heads/dev"], in: memberRepo)
        #expect(after == diverged)  // untouched
    }
}
