import Foundation
import Testing

@testable import NucleicCore

@Suite("Conflict detection")
struct ConflictDetectorTests {
    private func work(_ id: String, _ task: String, files: [String]) -> ActiveWork {
        ActiveWork(sessionID: SessionID(rawValue: id), title: id, task: task, changedFiles: files)
    }

    @Test func exactFileOverlapIsAConflict() {
        let candidate = ConflictCandidate(task: "tweak login copy", files: ["src/auth/Login.swift"])
        let active = [work("a", "auth refactor", files: ["src/auth/Login.swift", "README.md"])]
        let matches = ConflictDetector.detect(candidate, against: active)
        #expect(matches.count == 1)
        #expect(matches.first?.sessionID == SessionID(rawValue: "a"))
        #expect(matches.first?.sharedPaths == ["src/auth/Login.swift"])
    }

    @Test func directoryPrefixOverlapIsAConflict() {
        // Candidate names a directory; another session edits a file inside it.
        let candidate = ConflictCandidate(task: "rework auth", files: ["src/auth"])
        let active = [work("a", "fix login", files: ["src/auth/Login.swift"])]
        let matches = ConflictDetector.detect(candidate, against: active)
        #expect(matches.count == 1)
        // The more specific path is reported as the shared path.
        #expect(matches.first?.sharedPaths == ["src/auth/Login.swift"])
    }

    @Test func siblingDirectoriesDoNotOverlap() {
        // `src` must not match `src2/...` — componentwise comparison, not substring.
        let candidate = ConflictCandidate(task: "x", files: ["src"])
        let active = [work("a", "y", files: ["src2/Thing.swift"])]
        #expect(ConflictDetector.detect(candidate, against: active).isEmpty)
    }

    @Test func normalizesLeadingDotSlashAndTrailingSlash() {
        let candidate = ConflictCandidate(task: "x", files: ["./src/auth/"])
        let active = [work("a", "y", files: ["src/auth/Login.swift"])]
        #expect(ConflictDetector.detect(candidate, against: active).count == 1)
    }

    @Test func noOverlapYieldsNoMatches() {
        let candidate = ConflictCandidate(task: "edit docs", files: ["docs/README.md"])
        let active = [work("a", "auth refactor", files: ["src/auth/Login.swift"])]
        #expect(ConflictDetector.detect(candidate, against: active).isEmpty)
    }

    @Test func intentOverlapFlagsWhenNoFilesGiven() {
        // No files to compare, but the task descriptions strongly overlap.
        let candidate = ConflictCandidate(task: "redesign the billing subscription flow", files: [])
        let active = [work("a", "rework billing subscription invoices", files: [])]
        let matches = ConflictDetector.detect(candidate, against: active)
        #expect(matches.count == 1)
        #expect(matches.first?.sharedPaths.isEmpty == true)
    }

    @Test func unrelatedTasksDoNotIntentMatch() {
        let candidate = ConflictCandidate(task: "add a dark mode toggle", files: [])
        let active = [work("a", "optimize database query performance", files: [])]
        #expect(ConflictDetector.detect(candidate, against: active).isEmpty)
    }

    @Test func detectsAcrossMultipleSessions() {
        let candidate = ConflictCandidate(
            task: "x", files: ["src/auth/Login.swift", "src/ui/Home.swift"])
        let active = [
            work("a", "auth", files: ["src/auth/Login.swift"]),
            work("b", "docs", files: ["docs/guide.md"]),
            work("c", "home view", files: ["src/ui/Home.swift"]),
        ]
        let ids = Set(ConflictDetector.detect(candidate, against: active).map(\.sessionID))
        #expect(ids == [SessionID(rawValue: "a"), SessionID(rawValue: "c")])
    }
}

@Suite("Conflict coordinator")
struct ConflictCoordinatorTests {
    @Test func failsOpenWithoutArbiter() async {
        let coordinator = ConflictCoordinator()
        let resolution = await coordinator.arbitrate(
            sessionID: SessionID(rawValue: "s"), task: "anything", files: ["a.swift"])
        #expect(resolution.resolution == .noConflict)
    }

    @Test @MainActor func forwardsToArbiter() async {
        let coordinator = ConflictCoordinator()
        let arbiter = StubArbiter(resolution: .deferred)
        await coordinator.setArbiter(arbiter)
        let resolution = await coordinator.arbitrate(
            sessionID: SessionID(rawValue: "s"), task: "t", files: [])
        #expect(resolution.resolution == .deferred)
    }

    @Test func hostCommandFailsOpenWithoutArbiter() async {
        // No arbiter attached → host-command coordination must not block a command (HOST_EXEC).
        let coordinator = ConflictCoordinator()
        let clearance = await coordinator.beginHostCommand(
            sessionID: SessionID(rawValue: "s"), command: "swift build", override: false)
        #expect(clearance == .clear)
        await coordinator.endHostCommand(sessionID: SessionID(rawValue: "s"))  // no-op, must not crash
    }
}

/// Minimal arbiter for the coordinator forwarding test.
@MainActor
private final class StubArbiter: ConflictArbiter {
    let resolution: ConflictResolution
    init(resolution: ConflictResolution) { self.resolution = resolution }
    func arbitrate(sessionID: SessionID, task: String, files: [String]) async -> ArbitrationOutcome {
        ArbitrationOutcome(resolution)
    }
    func observeGitOp(sessionID: SessionID, argv: [String], exitCode: Int) async {}
    func observeGhOp(sessionID: SessionID, argv: [String], exitCode: Int) async {}
    func observeCommand(sessionID: SessionID, call: MCPApprovalServer.CommandReportCall) async {}
    func beginHostCommand(sessionID: SessionID, command: String, override: Bool) async -> HostCommandClearance {
        .clear
    }
    func endHostCommand(sessionID: SessionID) async {}
}
