import Testing
import Foundation
import AppKit
@testable import Alas

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

    /// Minimal valid 1x1 RGB PNG bytes generated offline. Using hardcoded
    /// bytes avoids relying on NSBitmapImageRep's colorspace handling, which
    /// emits warnings in headless test-host environments and may produce
    /// identical output for different color inputs.
    private enum PngFixture {
        /// 1x1 red pixel.
        static let red = Data(base64Encoded:
            "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"
        )!
        /// 1x1 blue pixel.
        static let blue = Data(base64Encoded:
            "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC"
        )!
        /// 1x1 green pixel.
        static let green = Data(base64Encoded:
            "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNg+M8AAAICAQB7CYF4AAAAAElFTkSuQmCC"
        )!
    }

    private func writeLFSPointer(
        for data: Data,
        named fileName: String,
        in repo: URL,
        storageDirectory: URL? = nil
    ) async throws {
        let oid = try await sha256Hex(data)
        let pointer = """
        version https://git-lfs.github.com/spec/v1
        oid sha256:\(oid)
        size \(data.count)

        """
        let objectDir = (storageDirectory ?? repo.appendingPathComponent(".git/lfs"))
            .appendingPathComponent("objects")
            .appendingPathComponent(String(oid.prefix(2)))
            .appendingPathComponent(String(oid.dropFirst(2).prefix(2)))
        try FileManager.default.createDirectory(at: objectDir, withIntermediateDirectories: true)
        try data.write(to: objectDir.appendingPathComponent(oid))
        try pointer.write(
            to: repo.appendingPathComponent(fileName),
            atomically: true,
            encoding: .utf8
        )
    }

    private func sha256Hex(_ data: Data) async throws -> String {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/usr/bin/shasum")
        process.arguments = ["-a", "256"]

        let input = Pipe()
        let output = Pipe()
        process.standardInput = input
        process.standardOutput = output
        process.standardError = Pipe()
        try process.run()
        input.fileHandleForWriting.write(data)
        try input.fileHandleForWriting.close()
        process.waitUntilExit()

        let stdout = output.fileHandleForReading.readDataToEndOfFile()
        let text = String(data: stdout, encoding: .utf8) ?? ""
        return String(text.prefix(while: { $0 != " " }))
    }

    @Test func loadsModifiedImagePair() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }

        let pngURL = repo.appendingPathComponent("logo.png")
        try PngFixture.red.write(to: pngURL)
        _ = try await Process.git(["add", "logo.png"], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "init"], cwd: repo)

        // Modify the file on disk.
        try PngFixture.blue.write(to: pngURL)

        let pair = try await GitService().imageDiffPair(
            worktreePath: repo, relativePath: "logo.png", staged: false
        )
        #expect(pair.kind == .modified)
        #expect(pair.before != nil)
        #expect(pair.after != nil)
        #expect(pair.oldPath == nil)
    }

    @Test func loadsAddedImagePair() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        // Need at least one prior commit so HEAD exists.
        try "seed\n".write(to: repo.appendingPathComponent("seed.txt"),
                           atomically: true, encoding: .utf8)
        _ = try await Process.git(["add", "."], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "seed"], cwd: repo)

        try PngFixture.green.write(to: repo.appendingPathComponent("new.png"))

        let pair = try await GitService().imageDiffPair(
            worktreePath: repo, relativePath: "new.png", staged: false
        )
        #expect(pair.kind == .added)
        #expect(pair.before == nil)
        #expect(pair.after != nil)
    }

    @Test func loadsDeletedImagePair() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        let pngURL = repo.appendingPathComponent("logo.png")
        try PngFixture.red.write(to: pngURL)
        _ = try await Process.git(["add", "."], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "init"], cwd: repo)

        try FileManager.default.removeItem(at: pngURL)

        let pair = try await GitService().imageDiffPair(
            worktreePath: repo, relativePath: "logo.png", staged: false
        )
        #expect(pair.kind == .deleted)
        #expect(pair.before != nil)
        #expect(pair.after == nil)
    }

    @Test func loadsRenamedImagePair() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        let oldURL = repo.appendingPathComponent("old.png")
        try PngFixture.red.write(to: oldURL)
        _ = try await Process.git(["add", "."], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "init"], cwd: repo)

        // `git mv` records a 100%-similarity rename in the index, so
        // git's rename detection identifies it regardless of similarity
        // threshold. The loader's job is to return both blobs and the
        // oldPath — whether the rename also includes a content delta is
        // a separate concern (git's similarity heuristic) and not what
        // this test is verifying.
        _ = try await Process.git(["mv", "old.png", "new.png"], cwd: repo)

        let pair = try await GitService().imageDiffPair(
            worktreePath: repo, relativePath: "new.png", staged: true
        )
        #expect(pair.kind == .renamed)
        #expect(pair.oldPath == "old.png")
        #expect(pair.before != nil)
        #expect(pair.after != nil)
    }

    @Test func loadsCommitImageDiffPairModified() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        let pngURL = repo.appendingPathComponent("logo.png")
        try PngFixture.red.write(to: pngURL)
        _ = try await Process.git(["add", "."], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "init"], cwd: repo)
        try PngFixture.blue.write(to: pngURL)
        _ = try await Process.git(["commit", "-q", "-am", "edit"], cwd: repo)
        let sha = try await Process.git(["rev-parse", "HEAD"], cwd: repo)
            .stdout.trimmingCharacters(in: .whitespacesAndNewlines)

        let file = CommitChangedFile(
            path: "logo.png", originalPath: nil, status: "M", add: 0, del: 0
        )
        let pair = try await GitService().imageDiffPairForCommit(
            worktreePath: repo, sha: sha, file: file
        )
        #expect(pair.kind == .modified)
        #expect(pair.before != nil)
        #expect(pair.after != nil)
    }

    @Test func loadsCommitImageDiffPairAddedAtInitialCommit() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        try PngFixture.green.write(to: repo.appendingPathComponent("new.png"))
        _ = try await Process.git(["add", "."], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "init"], cwd: repo)
        let sha = try await Process.git(["rev-parse", "HEAD"], cwd: repo)
            .stdout.trimmingCharacters(in: .whitespacesAndNewlines)

        let file = CommitChangedFile(
            path: "new.png", originalPath: nil, status: "A", add: 0, del: 0
        )
        let pair = try await GitService().imageDiffPairForCommit(
            worktreePath: repo, sha: sha, file: file
        )
        #expect(pair.kind == .added)
        #expect(pair.before == nil)
        #expect(pair.after != nil)
    }

    @Test func loadsCommitImageDiffPairRenamed() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }
        try PngFixture.red.write(to: repo.appendingPathComponent("old.png"))
        _ = try await Process.git(["add", "."], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "init"], cwd: repo)
        // git mv records a 100%-similarity rename in the index; no need
        // to also exercise the similarity heuristic.
        _ = try await Process.git(["mv", "old.png", "new.png"], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "rename"], cwd: repo)
        let sha = try await Process.git(["rev-parse", "HEAD"], cwd: repo)
            .stdout.trimmingCharacters(in: .whitespacesAndNewlines)

        let file = CommitChangedFile(
            path: "new.png", originalPath: "old.png", status: "R",
            add: 0, del: 0
        )
        let pair = try await GitService().imageDiffPairForCommit(
            worktreePath: repo, sha: sha, file: file
        )
        #expect(pair.kind == .renamed)
        #expect(pair.oldPath == "old.png")
        #expect(pair.before != nil)
        #expect(pair.after != nil)
    }

    @Test func picksUnstagedEntryWhenBothStagedAndUnstagedExist() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }

        // Need at least one prior commit so HEAD exists.
        try "seed\n".write(to: repo.appendingPathComponent("seed.txt"),
                           atomically: true, encoding: .utf8)
        _ = try await Process.git(["add", "."], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "seed"], cwd: repo)

        // Add a new image to the index (staged-add), then delete it from
        // the working tree. `git status` emits TWO rows for logo.png:
        //   (path: "logo.png", status: "A", stage: .staged)
        //   (path: "logo.png", status: "D", stage: .unstaged)
        let pngURL = repo.appendingPathComponent("logo.png")
        try PngFixture.red.write(to: pngURL)
        _ = try await Process.git(["add", "logo.png"], cwd: repo)
        try FileManager.default.removeItem(at: pngURL)

        // staged: false should pick the unstaged entry → .deleted.
        let unstaged = try await GitService().imageDiffPair(
            worktreePath: repo, relativePath: "logo.png", staged: false
        )
        #expect(unstaged.kind == .deleted)
        #expect(unstaged.before != nil)
        #expect(unstaged.after == nil)

        // staged: true should pick the staged entry → .added.
        let stagedPair = try await GitService().imageDiffPair(
            worktreePath: repo, relativePath: "logo.png", staged: true
        )
        #expect(stagedPair.kind == .added)
        #expect(stagedPair.before == nil)
        #expect(stagedPair.after != nil)
    }

    @Test func loadsLFSImageFromHeadPointerForUnstagedDiff() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }

        try await writeLFSPointer(for: PngFixture.red, named: "logo.png", in: repo)
        _ = try await Process.git(["add", "logo.png"], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "lfs image"], cwd: repo)

        try PngFixture.blue.write(to: repo.appendingPathComponent("logo.png"))

        let pair = try await GitService().imageDiffPair(
            worktreePath: repo, relativePath: "logo.png", staged: false
        )
        #expect(pair.kind == .modified)
        #expect(pair.before != nil)
        #expect(pair.after != nil)
    }

    @Test func loadsLFSImageFromIndexPointerForStagedDiff() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }

        try "seed\n".write(
            to: repo.appendingPathComponent("seed.txt"),
            atomically: true,
            encoding: .utf8
        )
        _ = try await Process.git(["add", "seed.txt"], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "seed"], cwd: repo)

        try await writeLFSPointer(for: PngFixture.green, named: "new.png", in: repo)
        _ = try await Process.git(["add", "new.png"], cwd: repo)

        let pair = try await GitService().imageDiffPair(
            worktreePath: repo, relativePath: "new.png", staged: true
        )
        #expect(pair.kind == .added)
        #expect(pair.before == nil)
        #expect(pair.after != nil)
    }

    @Test func loadsLFSImageFromConfiguredStorage() async throws {
        let repo = try await makeRepo()
        defer { try? FileManager.default.removeItem(at: repo) }

        _ = try await Process.git(["config", "lfs.storage", "custom-lfs"], cwd: repo)
        try await writeLFSPointer(
            for: PngFixture.red,
            named: "logo.png",
            in: repo,
            storageDirectory: repo.appendingPathComponent(".git/custom-lfs")
        )
        _ = try await Process.git(["add", "logo.png"], cwd: repo)
        _ = try await Process.git(["commit", "-q", "-m", "lfs image"], cwd: repo)

        try PngFixture.blue.write(to: repo.appendingPathComponent("logo.png"))

        let pair = try await GitService().imageDiffPair(
            worktreePath: repo, relativePath: "logo.png", staged: false
        )
        #expect(pair.kind == .modified)
        #expect(pair.before != nil)
        #expect(pair.after != nil)
    }
}
