import Foundation
import Testing

@testable import NucleicCore

@Suite struct BuildRunDetectorTests {
    /// Write `files` into a fresh temp dir and return its path.
    private func makeProject(_ files: [String: String]) throws -> String {
        let dir = FileManager.default.temporaryDirectory
            .appendingPathComponent("brd-\(UUID().uuidString)")
        try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        for (name, contents) in files {
            try contents.write(to: dir.appendingPathComponent(name), atomically: true, encoding: .utf8)
        }
        return dir.path
    }

    @Test func makefileTargetsClassifyBuildAndRun() throws {
        let makefile = """
        # a comment
        BIN = app
        build: deps
        \tswift build
        run:
        \t./app
        test:
        \tswift test
        clean:
        \trm -rf .build
        """
        let path = try makeProject(["Makefile": makefile])
        let commands = BuildRunDetector.detect(in: path)

        // `build` → build, `run` → run; `test`/`clean` are neither and are dropped.
        #expect(commands.contains { $0.kind == .build && $0.command == "make build" })
        #expect(commands.contains { $0.kind == .run && $0.command == "make run" })
        #expect(!commands.contains { $0.command.contains("test") })
        #expect(!commands.contains { $0.command.contains("clean") })
        // Build commands sort ahead of run commands.
        let kinds = commands.map(\.kind)
        #expect(kinds.firstIndex(of: .build)! < kinds.firstIndex(of: .run)!)
    }

    @Test func makefileSkipsRecipesVariablesAndPatternRules() throws {
        let makefile = """
        CFLAGS := -O2
        %.o: %.c
        \tcc -c $<
        all:
        \techo built
        """
        let path = try makeProject(["Makefile": makefile])
        let commands = BuildRunDetector.detect(in: path)
        // `all` is a build target; the pattern rule and variable line are ignored.
        #expect(commands.map(\.command) == ["make all"])
    }

    @Test func packageJsonScriptsBecomeNpmCommands() throws {
        let pkg = """
        {
          "name": "x",
          "scripts": {
            "build": "tsc",
            "dev": "vite",
            "start": "node server.js",
            "test": "vitest"
          }
        }
        """
        let path = try makeProject(["package.json": pkg])
        let commands = BuildRunDetector.detect(in: path)

        #expect(commands.contains { $0.kind == .build && $0.command == "npm run build" })
        #expect(commands.contains { $0.kind == .run && $0.command == "npm run dev" })
        // `start` uses the idiomatic `npm start`, and is classified as run.
        #expect(commands.contains { $0.kind == .run && $0.command == "npm start" })
        #expect(!commands.contains { $0.command.contains("test") })
    }

    @Test func swiftPackageGetsBuildAndRun() throws {
        let path = try makeProject(["Package.swift": "// swift-tools-version:5.9"])
        let commands = BuildRunDetector.detect(in: path)
        #expect(commands.contains { $0.kind == .build && $0.command == "swift build" })
        #expect(commands.contains { $0.kind == .run && $0.command == "swift run" })
    }

    @Test func emptyProjectYieldsNothing() throws {
        let path = try makeProject(["README.md": "hi"])
        #expect(BuildRunDetector.detect(in: path).isEmpty)
    }

    @Test func duplicateCommandsAreDeduped() throws {
        // A Makefile `build` and a package.json `build` produce distinct commands; but two
        // identical shell strings collapse to one.
        let path = try makeProject([
            "Cargo.toml": "[package]\nname = \"x\"",
            "Makefile": "build:\n\tcargo build\n",
        ])
        let commands = BuildRunDetector.detect(in: path)
        let strings = commands.map(\.command)
        #expect(Set(strings).count == strings.count)  // no exact dupes
    }
}
