import Foundation
import Testing

@testable import NucleicCore

/// Locks the newest-wins write-back rule that keeps the sandbox's Claude login coherent with the
/// Keychain (LOGIN SYNC). Only the I/O-free core is exercised here — the real Keychain read/write
/// would touch the developer's login item, so `ClaudeLoginKeychain` splits the decision
/// (`shouldWriteBack`) and the parsing (`expiresAt`/`isValidLogin`) out for exactly this.
struct ClaudeLoginKeychainTests {
    /// A credentials-file JSON like the one `claude` writes, with the given expiry and tokens.
    private static func login(expiresAt: Double, access: String = "acc", refresh: String = "ref") -> String {
        """
        {"claudeAiOauth":{"accessToken":"\(access)","refreshToken":"\(refresh)",\
        "expiresAt":\(Int(expiresAt)),"scopes":["user:inference"],"subscriptionType":"max"}}
        """
    }

    // MARK: - Test-process guard

    /// The test bootstrap disarms all real Keychain I/O so a headless suite never touches the
    /// developer's actual login item. `.isolatedContainerSettings` installs the process-wide guards;
    /// this pins that wiring.
    @Test(.isolatedContainerSettings) func testProcessDisablesRealKeychainIO() {
        #expect(ClaudeLoginKeychain.disabled)
    }

    // MARK: - Parsing

    @Test func parsesExpiresAtAndValidatesLogin() {
        let json = Self.login(expiresAt: 1_700_000_000_000)
        #expect(ClaudeLoginKeychain.expiresAt(json) == 1_700_000_000_000)
        #expect(ClaudeLoginKeychain.isValidLogin(json))
        #expect(ClaudeLoginKeychain.accessToken(json) == "acc")
    }

    @Test func expiresAtToleratesNumericString() {
        let json = """
        {"claudeAiOauth":{"accessToken":"a","refreshToken":"r","expiresAt":"1700000000000"}}
        """
        #expect(ClaudeLoginKeychain.expiresAt(json) == 1_700_000_000_000)
    }

    @Test func invalidLoginsAreRejected() {
        // Missing refresh token, empty access token, wrong shape, and junk all fail validation.
        #expect(!ClaudeLoginKeychain.isValidLogin(#"{"claudeAiOauth":{"accessToken":"a"}}"#))
        #expect(!ClaudeLoginKeychain.isValidLogin(#"{"claudeAiOauth":{"accessToken":"","refreshToken":"r"}}"#))
        #expect(!ClaudeLoginKeychain.isValidLogin(#"{"other":{}}"#))
        #expect(!ClaudeLoginKeychain.isValidLogin("not json"))
        #expect(ClaudeLoginKeychain.expiresAt("not json") == nil)
    }

    @Test func removesFullCredentialAfterBrokerStaging() throws {
        let directory = FileManager.default.temporaryDirectory
            .appendingPathComponent("nucleic-claude-broker-\(UUID().uuidString)")
        try FileManager.default.createDirectory(
            at: directory, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: directory) }
        let credential = directory.appendingPathComponent(".credentials.json")
        try Self.login(expiresAt: 2_000).write(
            to: credential, atomically: true, encoding: .utf8)

        ClaudeLoginKeychain.removeExportedCredential(from: directory.path)
        #expect(!FileManager.default.fileExists(atPath: credential.path))
    }

    // MARK: - Newest-wins decision

    @Test func writesBackWhenCandidateIsStrictlyNewer() {
        let current = Self.login(expiresAt: 1_000, refresh: "old")
        let candidate = Self.login(expiresAt: 2_000, refresh: "rotated")
        #expect(ClaudeLoginKeychain.shouldWriteBack(candidate: candidate, current: current))
    }

    @Test func skipsWhenCandidateIsOlderOrEqual() {
        let current = Self.login(expiresAt: 2_000)
        #expect(!ClaudeLoginKeychain.shouldWriteBack(candidate: Self.login(expiresAt: 1_000), current: current))
        // Equal expiry is not newer — a re-export of the same login must not thrash the Keychain.
        #expect(!ClaudeLoginKeychain.shouldWriteBack(candidate: Self.login(expiresAt: 2_000), current: current))
    }

    @Test func skipsIdenticalCredential() {
        let same = Self.login(expiresAt: 2_000)
        #expect(!ClaudeLoginKeychain.shouldWriteBack(candidate: same, current: same))
    }

    @Test func skipsInvalidCandidate() {
        let current = Self.login(expiresAt: 1_000)
        // A truncated/half-written file must never be pushed back, even though it "looks newer".
        #expect(!ClaudeLoginKeychain.shouldWriteBack(candidate: #"{"claudeAiOauth":{"accessToken":"a"}}"#, current: current))
    }

    @Test func adoptsWhenMissingButSkipsUnparseableCurrent() {
        let candidate = Self.login(expiresAt: 2_000)
        // A fresh Nucleic store adopts a valid container login; an unparseable existing value is
        // preserved because we cannot prove the candidate is newer.
        #expect(ClaudeLoginKeychain.shouldWriteBack(candidate: candidate, current: nil))
        #expect(!ClaudeLoginKeychain.shouldWriteBack(candidate: candidate, current: "not json"))
    }

    // MARK: - Non-destructive export decision

    @Test func exportsWhenNoExistingContainerFile() {
        // First seed (or a since-deleted file): always write Nucleic's login in.
        #expect(ClaudeLoginKeychain.shouldExport(keychain: Self.login(expiresAt: 2_000), toExistingFile: nil))
    }

    @Test func exportsWhenExistingFileIsInvalidOrUnparseable() {
        let keychain = Self.login(expiresAt: 2_000)
        // A truncated/half-written container file is repaired from Nucleic's store, not preserved.
        #expect(ClaudeLoginKeychain.shouldExport(
            keychain: keychain, toExistingFile: #"{"claudeAiOauth":{"accessToken":"a"}}"#))
        #expect(ClaudeLoginKeychain.shouldExport(keychain: keychain, toExistingFile: "not json"))
    }

    @Test func exportsWhenKeychainIsNewerOrEqual() {
        // Host (or another session) rotated the token: the Keychain is fresher, so push it in.
        #expect(ClaudeLoginKeychain.shouldExport(
            keychain: Self.login(expiresAt: 3_000, refresh: "rotated"),
            toExistingFile: Self.login(expiresAt: 2_000, refresh: "old")))
        // Equal expiry re-exports harmlessly (same lineage) — matches the write-back's equality rule.
        #expect(ClaudeLoginKeychain.shouldExport(
            keychain: Self.login(expiresAt: 2_000), toExistingFile: Self.login(expiresAt: 2_000)))
    }

    @Test func preservesStrictlyFresherContainerFile() {
        // The core fix: the sandbox refreshed in place but the write-back to the Claude-owned
        // Keychain didn't land, so the file is strictly newer. Exporting the stale Keychain token
        // would strand the sandbox — keep the file.
        #expect(!ClaudeLoginKeychain.shouldExport(
            keychain: Self.login(expiresAt: 1_000, refresh: "revoked"),
            toExistingFile: Self.login(expiresAt: 2_000, refresh: "live")))
    }

    @Test func exportsWhenKeychainIsUnparseable() {
        // Can't prove the file is fresher, so fall through to the legacy overwrite behavior.
        #expect(ClaudeLoginKeychain.shouldExport(
            keychain: "not json", toExistingFile: Self.login(expiresAt: 2_000)))
    }

}
