import Foundation
import Testing
import NucleicProtocol

@testable import NucleicCore

/// Mesh P5 — git bundle round-trip + repo-shape/identity probes against real temp repos.
@Suite struct TransferGitTests {

    /// A source worktree with a committed change bundles, and the destination (a clone sharing
    /// the base commit) verifies + fetches + checks it out with byte-identical contents.
    @Test func bundleRoundTripPrerequisiteBased() async throws {
        let source = try await GitTestRepo()
        defer { source.cleanup() }
        let wm = GitWorktreeManager()

        // Destination: an independent clone taken BEFORE the session branch exists — it shares
        // the base commit (main) but not the session's later work (the realistic case).
        let destContainer = (NSTemporaryDirectory() as NSString)
            .appendingPathComponent("nucleic-dest-\(UUID().uuidString)")
        let destRoot = (destContainer as NSString).appendingPathComponent("repo")
        defer { try? FileManager.default.removeItem(atPath: destContainer) }
        try await source.run(["clone", source.root, destRoot], in: NSTemporaryDirectory())
        let destProject = Project(name: "dest", rootPath: destRoot, defaultBranch: "main")

        // Source: a session worktree + branch with a new commit past the shared base.
        let wt = try await wm.create(for: SessionID(rawValue: "s1"), in: source.project(),
                                     base: "main", slug: "feature")
        try source.write("feature.txt", "hello from the source\n", in: wt.path)
        try await source.run(["add", "-A"], in: wt.path)
        try await source.run(["commit", "-m", "add feature"], in: wt.path)
        let tip = try await source.revParse("HEAD", in: wt.path)

        // Destination has the base commit but not the branch tip → prerequisite-based bundle.
        let have = try await wm.presentSHAs([wt.baseSHA, tip], in: destProject)
        #expect(have.contains(wt.baseSHA))
        #expect(!have.contains(tip))  // the branch tip only exists on the source

        let bundle = URL(fileURLWithPath: (destContainer as NSString).appendingPathComponent("x.bundle"))
        try await wm.createTransferBundle(wt, in: source.project(), haveSHAs: have, to: bundle)
        #expect(FileManager.default.fileExists(atPath: bundle.path))

        #expect(try await wm.branchExists("nucleic/feature", in: destProject) == false)
        let destWtPath = (destContainer as NSString).appendingPathComponent("wt-feature")
        let imported = try await wm.createFromTransfer(
            bundle: bundle, branch: "nucleic/feature", baseSHA: wt.baseSHA,
            destPath: destWtPath, in: destProject)

        #expect(imported.branch == "nucleic/feature")
        #expect(imported.baseSHA == wt.baseSHA)
        // The branch tip and file contents match the source exactly.
        #expect(try await source.revParse("HEAD", in: destWtPath) == tip)
        #expect(source.read("feature.txt", in: destWtPath) == "hello from the source\n")
        #expect(try await wm.branchExists("nucleic/feature", in: destProject) == true)
    }

    /// When the destination shares no history (an empty repo), the bundle falls back to
    /// prerequisite-free (full history) and still imports.
    @Test func bundleRoundTripPrerequisiteFreeFallback() async throws {
        let source = try await GitTestRepo()
        defer { source.cleanup() }
        let wm = GitWorktreeManager()
        let wt = try await wm.create(for: SessionID(rawValue: "s1"), in: source.project(),
                                     base: "main", slug: "feature")
        try source.write("f.txt", "content\n", in: wt.path)
        try await source.run(["add", "-A"], in: wt.path)
        try await source.run(["commit", "-m", "work"], in: wt.path)

        // A brand-new empty repo shares nothing.
        let destContainer = (NSTemporaryDirectory() as NSString)
            .appendingPathComponent("nucleic-dest-\(UUID().uuidString)")
        let destRoot = (destContainer as NSString).appendingPathComponent("repo")
        defer { try? FileManager.default.removeItem(atPath: destContainer) }
        try FileManager.default.createDirectory(atPath: destRoot, withIntermediateDirectories: true)
        try await source.run(["init", "-b", "main"], in: destRoot)
        let destProject = Project(name: "dest", rootPath: destRoot, defaultBranch: "main")

        // No shared SHAs → the have list is empty → full bundle.
        let have = try await wm.presentSHAs([wt.baseSHA], in: destProject)
        #expect(have.isEmpty)
        let bundle = URL(fileURLWithPath: (destContainer as NSString).appendingPathComponent("x.bundle"))
        try await wm.createTransferBundle(wt, in: source.project(), haveSHAs: have, to: bundle)

        let destWtPath = (destContainer as NSString).appendingPathComponent("wt")
        let imported = try await wm.createFromTransfer(
            bundle: bundle, branch: "nucleic/feature", baseSHA: wt.baseSHA,
            destPath: destWtPath, in: destProject)
        #expect(source.read("f.txt", in: imported.path) == "content\n")
    }

    /// A dirty worktree is auto-committed by `finalize(.auto)` before bundling, so uncommitted
    /// work still travels — the quiesce path.
    @Test func finalizeCapturesUncommittedWorkBeforeBundle() async throws {
        let source = try await GitTestRepo()
        defer { source.cleanup() }
        let wm = GitWorktreeManager()
        let wt = try await wm.create(for: SessionID(rawValue: "s1"), in: source.project(),
                                     base: "main", slug: "wip")
        // Uncommitted (tracked + untracked) work.
        try source.write("dirty.txt", "unsaved\n", in: wt.path)
        try await wm.finalize(wt, commit: .auto(message: "nucleic: transfer snapshot"), onCommit: nil)

        let destContainer = (NSTemporaryDirectory() as NSString)
            .appendingPathComponent("nucleic-dest-\(UUID().uuidString)")
        let destRoot = (destContainer as NSString).appendingPathComponent("repo")
        defer { try? FileManager.default.removeItem(atPath: destContainer) }
        try await source.run(["clone", source.root, destRoot], in: NSTemporaryDirectory())
        let destProject = Project(name: "dest", rootPath: destRoot, defaultBranch: "main")

        let have = try await wm.presentSHAs([wt.baseSHA], in: destProject)
        let bundle = URL(fileURLWithPath: (destContainer as NSString).appendingPathComponent("x.bundle"))
        try await wm.createTransferBundle(wt, in: source.project(), haveSHAs: have, to: bundle)
        let destWt = (destContainer as NSString).appendingPathComponent("wt")
        let imported = try await wm.createFromTransfer(
            bundle: bundle, branch: "nucleic/wip", baseSHA: wt.baseSHA, destPath: destWt, in: destProject)
        #expect(source.read("dirty.txt", in: imported.path) == "unsaved\n")
    }

    @Test func discardAfterTransferRemovesWorktreeAndBranch() async throws {
        let source = try await GitTestRepo()
        defer { source.cleanup() }
        let wm = GitWorktreeManager()
        let wt = try await wm.create(for: SessionID(rawValue: "s1"), in: source.project(),
                                     base: "main", slug: "gone")
        try source.write("x.txt", "y\n", in: wt.path)
        try await source.run(["add", "-A"], in: wt.path)
        try await source.run(["commit", "-m", "unmerged work"], in: wt.path)

        // Force-discards even though the branch has unmerged commits (they live on the dest now).
        try await wm.discardAfterTransfer(wt, in: source.project())
        #expect(!FileManager.default.fileExists(atPath: wt.path))
        #expect(try await wm.branchExists("nucleic/gone", in: source.project()) == false)
    }

    // MARK: repo shape + identity probes

    @Test func repoShapeCleanRepoIsTransferable() async throws {
        let repo = try await GitTestRepo()
        defer { repo.cleanup() }
        let shape = try await TransferGitProbe().repoShape(rootPath: repo.root)
        #expect(shape.rejectReason == nil)
        #expect(!shape.isShallow && !shape.hasSubmodules && !shape.usesLFS)
    }

    @Test func repoShapeDetectsShallowSubmodulesAndLFS() async throws {
        // Submodules: a .gitmodules file at the root.
        let subRepo = try await GitTestRepo()
        defer { subRepo.cleanup() }
        try subRepo.write(".gitmodules", "[submodule \"x\"]\n  path = x\n  url = ./x\n")
        let subShape = try await TransferGitProbe().repoShape(rootPath: subRepo.root)
        #expect(subShape.hasSubmodules)
        #expect(subShape.rejectReason == .submoduleRepo)

        // LFS: a root .gitattributes wiring the lfs filter.
        let lfsRepo = try await GitTestRepo()
        defer { lfsRepo.cleanup() }
        try lfsRepo.write(".gitattributes", "*.bin filter=lfs diff=lfs merge=lfs -text\n")
        let lfsShape = try await TransferGitProbe().repoShape(rootPath: lfsRepo.root)
        #expect(lfsShape.usesLFS)
        #expect(lfsShape.rejectReason == .lfsRepo)

        // Shallow: a depth-1 clone.
        let deep = try await GitTestRepo()
        defer { deep.cleanup() }
        try deep.write("a.txt", "1\n"); try await deep.run(["add", "-A"]); try await deep.run(["commit", "-m", "two"])
        let shallowContainer = (NSTemporaryDirectory() as NSString)
            .appendingPathComponent("nucleic-shallow-\(UUID().uuidString)")
        let shallowRoot = (shallowContainer as NSString).appendingPathComponent("repo")
        defer { try? FileManager.default.removeItem(atPath: shallowContainer) }
        try await deep.run(["clone", "--depth", "1", "file://\(deep.root)", shallowRoot], in: NSTemporaryDirectory())
        let shallowShape = try await TransferGitProbe().repoShape(rootPath: shallowRoot)
        #expect(shallowShape.isShallow)
        #expect(shallowShape.rejectReason == .shallowRepo)
    }

    @Test func repoIdentityRootCommitAndNormalizedRemote() async throws {
        let repo = try await GitTestRepo()
        defer { repo.cleanup() }
        try await repo.run(["remote", "add", "origin", "git@github.com:Me/Proj.git"])
        let identity = try await TransferGitProbe().repoIdentity(rootPath: repo.root)
        #expect(identity.rootCommitSHA == (try await repo.revParse("HEAD")))  // single commit = its own root
        #expect(identity.normalizedRemote == "github.com/me/proj")
    }

    @Test func normalizeRemoteFormsCollapseToTheSameIdentity() {
        let forms = [
            "https://github.com/Me/Proj.git",
            "https://github.com/Me/Proj/",
            "git@github.com:Me/Proj.git",
            "ssh://git@github.com/Me/Proj",
        ]
        let normalized = Set(forms.map(TransferGitProbe.normalizeRemote))
        #expect(normalized == ["github.com/me/proj"])
    }
}
