import Foundation
import Testing

@testable import NucleicCore

/// Locks the newest-wins rule that keeps a sandboxed Codex login coherent with the host's
/// `~/.codex/auth.json` (LOGIN SYNC). Only the I/O-free core is exercised — the real file copy
/// touches the developer's home — so `CodexAuthFile` splits the decision (`shouldReplace`) and the
/// parsing (`lastRefresh`/`isValidLogin`) out for exactly this.
struct CodexAuthFileTests {
    /// An `auth.json` like the one `codex` writes, with the given `last_refresh` and refresh token.
    private static func login(lastRefresh: String, refresh: String = "ref", apiKey: String = "") -> String {
        """
        {"OPENAI_API_KEY":"\(apiKey)","tokens":{"id_token":"id","access_token":"acc",\
        "refresh_token":"\(refresh)"},"last_refresh":"\(lastRefresh)"}
        """
    }

    // MARK: - Parsing

    @Test func parsesLastRefreshWithAndWithoutFractionalSeconds() {
        // Codex emits fractional seconds; a re-login may not. Both parse, and the fractional instant
        // sorts strictly after the whole-second one at the same minute.
        let frac = Self.lastRefresh(Self.login(lastRefresh: "2026-07-09T10:56:59.068487Z"))
        let plain = Self.lastRefresh(Self.login(lastRefresh: "2026-07-09T10:56:59Z"))
        #expect(frac != nil && plain != nil)
        #expect(frac! > plain!)
    }

    private static func lastRefresh(_ json: String) -> Double? { CodexAuthFile.lastRefresh(json) }

    @Test func validatesLoginRequiresNonEmptyRefreshToken() {
        #expect(CodexAuthFile.isValidLogin(Self.login(lastRefresh: "2026-07-09T10:56:59Z")))
        // API-key-only auth.json (no tokens) has no refresh lineage to reconcile.
        #expect(!CodexAuthFile.isValidLogin(#"{"OPENAI_API_KEY":"sk-abc","tokens":{}}"#))
        #expect(!CodexAuthFile.isValidLogin(#"{"tokens":{"refresh_token":""}}"#))
        #expect(!CodexAuthFile.isValidLogin("not json"))
        #expect(CodexAuthFile.lastRefresh("not json") == nil)
    }

    // MARK: - Newest-wins decision

    @Test func replacesWhenCandidateIsStrictlyNewer() {
        let current = Self.login(lastRefresh: "2026-07-09T10:00:00Z", refresh: "old")
        let candidate = Self.login(lastRefresh: "2026-07-09T11:00:00Z", refresh: "rotated")
        #expect(CodexAuthFile.shouldReplace(candidate: candidate, current: current))
    }

    @Test func skipsWhenCandidateIsOlderOrEqual() {
        let current = Self.login(lastRefresh: "2026-07-09T11:00:00Z")
        #expect(!CodexAuthFile.shouldReplace(
            candidate: Self.login(lastRefresh: "2026-07-09T10:00:00Z"), current: current))
        // Equal timestamp is not newer — re-seeding the same login must not thrash the file.
        #expect(!CodexAuthFile.shouldReplace(
            candidate: Self.login(lastRefresh: "2026-07-09T11:00:00Z"), current: current))
    }

    @Test func skipsIdenticalCredential() {
        let same = Self.login(lastRefresh: "2026-07-09T11:00:00Z")
        #expect(!CodexAuthFile.shouldReplace(candidate: same, current: same))
    }

    @Test func skipsInvalidOrUndatedCandidate() {
        let current = Self.login(lastRefresh: "2026-07-09T10:00:00Z")
        // A truncated/half-written file, or one missing last_refresh, must never be propagated even
        // though it might "look" newer.
        #expect(!CodexAuthFile.shouldReplace(candidate: #"{"tokens":{"refresh_token":"r"}}"#, current: current))
        #expect(!CodexAuthFile.shouldReplace(candidate: "not json", current: current))
    }

    @Test func seedsWhenDestinationIsMissingOrUnparseable() {
        // Unlike the Claude Keychain (which never fabricates), the Codex path is a file copy used to
        // seed the container: a nil destination (not yet created) or an unparseable one is stale, so a
        // valid candidate wins. This is what pushes the host login into a fresh container home.
        let candidate = Self.login(lastRefresh: "2026-07-09T11:00:00Z")
        #expect(CodexAuthFile.shouldReplace(candidate: candidate, current: nil))
        #expect(CodexAuthFile.shouldReplace(candidate: candidate, current: "not json"))
    }
}
