import Testing
import Foundation
@testable import Alas

@Suite(.serialized)
struct GitServiceStagingTests {
    private func makeRepo() async throws -> URL {
        let dir = FileManager.default.temporaryDirectory
            .appendingPathComponent("alas-stg-\(UUID().uuidString)")
        try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        _ = try await Process.git(["init", "-q", "-b", "main"], cwd: dir)
        _ = try await Process.git(["config", "user.email", "t@e"], cwd: dir)
        _ = try await Process.git(["config", "user.name", "t"], cwd: dir)
        return dir
    }

    private func writeFile(_ repo: URL, _ name: String, _ contents: String) throws {
        try contents.write(
            to: repo.appendingPathComponent(name),
            atomically: true,
            encoding: .utf8
        )
    }

    @Test func stageMovesFileToIndex() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        try writeFile(repo, "a.txt", "hello\n")
        let svc = GitService()
        try await svc.stage(worktreePath: repo, files: ["a.txt"])
        let status = try await svc.status(worktreePath: repo)
        #expect(status.contains { $0.path == "a.txt" && $0.stage == .staged })
    }

    @Test func unstageRemovesFromIndex() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        try writeFile(repo, "a.txt", "hello\n")
        _ = try await Process.git(["add", "a.txt"], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "init"], cwd: repo)
        try writeFile(repo, "a.txt", "hello world\n")
        _ = try await Process.git(["add", "a.txt"], cwd: repo)

        let svc = GitService()
        try await svc.unstage(worktreePath: repo, files: ["a.txt"])
        let status = try await svc.status(worktreePath: repo)
        #expect(status.contains { $0.path == "a.txt" && $0.stage == .unstaged })
        #expect(!status.contains { $0.path == "a.txt" && $0.stage == .staged })
    }

    @Test func unstageOnUnbornBranchUsesReset() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        // Create a fresh repo with no commits and stage a new file.
        let unborn = FileManager.default.temporaryDirectory
            .appendingPathComponent("alas-unborn-\(UUID().uuidString)")
        try FileManager.default.createDirectory(at: unborn, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: unborn) }
        _ = try await Process.git(["init", "-q", "-b", "main"], cwd: unborn)
        try writeFile(unborn, "a.txt", "x\n")
        _ = try await Process.git(["add", "a.txt"], cwd: unborn)

        let svc = GitService()
        try await svc.unstage(worktreePath: unborn, files: ["a.txt"])
        let status = try await svc.status(worktreePath: unborn)
        // After unstage on unborn HEAD, the file should be untracked (status "A" / unstaged).
        #expect(status.contains { $0.path == "a.txt" && $0.stage == .unstaged })
    }

    @Test func stageAllPicksUpEveryUnstagedPath() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        try writeFile(repo, "a.txt", "1")
        try writeFile(repo, "b.txt", "2")
        let svc = GitService()
        try await svc.stageAll(worktreePath: repo, files: ["a.txt", "b.txt"])
        let status = try await svc.status(worktreePath: repo)
        #expect(status.filter { $0.stage == .staged }.count == 2)
    }

    @Test func statusExpandsUntrackedDirectoriesIntoFiles() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        let dir = repo.appendingPathComponent(".clawpatch")
        try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        try writeFile(repo, ".clawpatch/manifest.json", "{}\n")
        try FileManager.default.createDirectory(
            at: dir.appendingPathComponent("nested"),
            withIntermediateDirectories: true
        )
        try writeFile(repo, ".clawpatch/nested/change.json", "{\"ok\":true}\n")

        let status = try await GitService().status(worktreePath: repo)
        let paths = status.map(\.path).sorted()

        #expect(paths == [
            ".clawpatch/manifest.json",
            ".clawpatch/nested/change.json"
        ])
        #expect(status.allSatisfy { $0.status == "A" && $0.stage == .unstaged })
    }

    @Test func unstageAllClearsIndex() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        try writeFile(repo, "a.txt", "1")
        try writeFile(repo, "b.txt", "2")
        _ = try await Process.git(["add", "a.txt", "b.txt"], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "init"], cwd: repo)
        try writeFile(repo, "a.txt", "11")
        try writeFile(repo, "b.txt", "22")
        _ = try await Process.git(["add", "a.txt", "b.txt"], cwd: repo)

        let svc = GitService()
        try await svc.unstageAll(worktreePath: repo, files: ["a.txt", "b.txt"])
        let status = try await svc.status(worktreePath: repo)
        #expect(status.filter { $0.stage == .staged }.isEmpty)
    }

    @Test func stageDeletedFileRecordsDeletion() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        try writeFile(repo, "a.txt", "x\n")
        _ = try await Process.git(["add", "a.txt"], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "init"], cwd: repo)
        try FileManager.default.removeItem(at: repo.appendingPathComponent("a.txt"))

        let svc = GitService()
        try await svc.stage(worktreePath: repo, files: ["a.txt"])
        let status = try await svc.status(worktreePath: repo)
        #expect(status.contains { $0.path == "a.txt" && $0.stage == .staged && $0.status == "D" })
    }

    @Test func commitWritesNewHeadWithSubjectAndBody() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        try writeFile(repo, "a.txt", "x\n")
        let svc = GitService()
        try await svc.stage(worktreePath: repo, files: ["a.txt"])
        try await svc.commit(
            worktreePath: repo,
            subject: "feat: add a",
            body: "explanation goes here",
            amend: false
        )
        let log = try await Process.git(["log", "-1", "--pretty=format:%s%n%b"], cwd: repo)
        #expect(log.stdout.contains("feat: add a"))
        #expect(log.stdout.contains("explanation goes here"))
    }

    @Test func amendRewritesHead() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        try writeFile(repo, "a.txt", "x\n")
        let svc = GitService()
        try await svc.stage(worktreePath: repo, files: ["a.txt"])
        try await svc.commit(worktreePath: repo, subject: "wip", body: "", amend: false)

        try writeFile(repo, "b.txt", "y\n")
        try await svc.stage(worktreePath: repo, files: ["b.txt"])
        try await svc.commit(
            worktreePath: repo,
            subject: "feat: a and b",
            body: "single combined commit",
            amend: true
        )

        let log = try await Process.git(["log", "--pretty=format:%s"], cwd: repo)
        let subjects = log.stdout.split(separator: "\n").map(String.init)
        #expect(subjects == ["feat: a and b"])
    }

    @Test func commitFailsWhenNothingStaged() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        let svc = GitService()
        await #expect(throws: (any Error).self) {
            try await svc.commit(worktreePath: repo, subject: "noop", body: "", amend: false)
        }
    }
}
