import Foundation
import Testing

@testable import NucleicCore

/// End-to-end autoship: drive real worktrees through the MergeQueue with a
/// GitWorktreeManager-backed integrator (the same finalize→integrate path AppStore uses),
/// proving parallel ships land safely and a conflict stops without overwriting.
@Suite("Autoship merge — git-backed, through the queue")
struct AutoshipMergeTests {
    private func manager() -> GitWorktreeManager {
        GitWorktreeManager(now: { Date(timeIntervalSince1970: 1_700_000_000) })
    }

    /// Build a queue whose integrator finalizes + squash-merges the given worktrees,
    /// mirroring AppStore.performShip but at the manager level (no SessionController).
    private func queue(
        manager: GitWorktreeManager, project: Project, worktrees: [SessionID: Worktree]
    ) async -> MergeQueue {
        let q = MergeQueue()
        await q.setIntegrator { req in
            guard let wt = worktrees[req.sessionID] else { return .skipped("no worktree") }
            do {
                try await manager.finalize(wt, commit: .auto(message: "work \(req.sessionID.rawValue)"))
                let result = try await manager.integrate(
                    wt, in: project, into: req.target, strategy: req.strategy, message: nil)
                switch result {
                case .clean(_, let commit): return .merged(commit: commit)
                case .conflicted(let paths): return .conflicted(paths.map(\.path))
                }
            } catch { return .failed("\(error)") }
        }
        return q
    }

    private func drainTerminals(_ stream: AsyncStream<ShipUpdate>, count: Int) async -> [ShipUpdate] {
        var out: [ShipUpdate] = []
        for await update in stream where update.status.isTerminal {
            out.append(update)
            if out.count == count { break }
        }
        return out
    }

    private func req(_ session: SessionID, _ project: Project) -> ShipRequest {
        ShipRequest(sessionID: session, projectID: project.id, target: "main", strategy: .squash)
    }

    @Test func parallelShipsBothLandSerialized() async throws {
        let repo = try await GitTestRepo()
        defer { repo.cleanup() }
        let manager = manager()
        let project = repo.project()

        let s1 = SessionID(rawValue: "s1"), s2 = SessionID(rawValue: "s2")
        let wt1 = try await manager.create(for: s1, in: project, base: "main", slug: "alpha")
        let wt2 = try await manager.create(for: s2, in: project, base: "main", slug: "beta")
        try repo.write("alpha.txt", "a\n", in: wt1.path)
        try repo.write("beta.txt", "b\n", in: wt2.path)

        let queue = await queue(manager: manager, project: project, worktrees: [s1: wt1, s2: wt2])
        let stream = await queue.updates()
        #expect(await queue.enqueue(req(s1, project)))
        #expect(await queue.enqueue(req(s2, project)))
        let terminals = await drainTerminals(stream, count: 2)

        // Both shipped, in FIFO order, into the same target — no work lost.
        #expect(terminals.map(\.sessionID) == [s1, s2])
        for t in terminals {
            guard case .merged = t.status else {
                Issue.record("expected merged, got \(t.status)")
                return
            }
        }
        #expect(repo.read("alpha.txt") == "a\n")
        #expect(repo.read("beta.txt") == "b\n")
        let mainStatus = try await repo.run(["status", "--porcelain"]).stdout
        #expect(mainStatus.isEmpty)
    }

    @Test func conflictingSecondShipStopsWithoutOverwriting() async throws {
        let repo = try await GitTestRepo()
        defer { repo.cleanup() }
        let manager = manager()
        let project = repo.project()

        let s1 = SessionID(rawValue: "s1"), s2 = SessionID(rawValue: "s2")
        let wt1 = try await manager.create(for: s1, in: project, base: "main", slug: "first")
        let wt2 = try await manager.create(for: s2, in: project, base: "main", slug: "second")
        // Both add the SAME new file with different content → add/add conflict at merge.
        try repo.write("shared.txt", "from-one\n", in: wt1.path)
        try repo.write("shared.txt", "from-two\n", in: wt2.path)

        let queue = await queue(manager: manager, project: project, worktrees: [s1: wt1, s2: wt2])
        let stream = await queue.updates()
        #expect(await queue.enqueue(req(s1, project)))
        #expect(await queue.enqueue(req(s2, project)))
        let terminals = await drainTerminals(stream, count: 2)

        // First lands; second conflicts.
        guard case .merged(let firstCommit) = terminals[0].status else {
            Issue.record("expected first merged, got \(terminals[0].status)")
            return
        }
        guard case .conflicted(let paths) = terminals[1].status else {
            Issue.record("expected second conflicted, got \(terminals[1].status)")
            return
        }
        #expect(paths.contains("shared.txt"))
        // The first session's work is untouched: main still at its commit, its content intact,
        // and the tree is clean (the conflicted merge was aborted, not left half-applied).
        #expect(try await repo.revParse("HEAD") == firstCommit)
        #expect(repo.read("shared.txt") == "from-one\n")
        let mainStatus = try await repo.run(["status", "--porcelain"]).stdout
        #expect(mainStatus.isEmpty)
    }
}
