import Foundation
import Testing

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif

@testable import NucleicCore

struct CodexOAuthTests {
    // MARK: - PKCE / URL

    @Test func challengeIsBase64URLSHA256OfVerifier() {
        let pkce = CodexOAuth.PKCE(verifier: "codex-verifier-abcdef", state: "st")
        let expected = CodexOAuth.base64URL(Data(SHA256.hash(data: Data(pkce.verifier.utf8))))
        #expect(pkce.challenge == expected)
        #expect(!pkce.challenge.contains("="))
    }

    @Test func authorizeURLTargetsOpenAIWithFixedRedirect() throws {
        let pkce = CodexOAuth.PKCE(verifier: "v", state: "the-state")
        let url = CodexOAuth.authorizeURL(pkce: pkce)
        let components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false))
        let items = Dictionary(
            uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value) })
        #expect(url.absoluteString.hasPrefix(CodexOAuth.authorizeEndpoint))
        #expect(items["client_id"] == CodexOAuth.clientID)
        #expect(items["redirect_uri"] == "http://localhost:1455/auth/callback")
        #expect(items["code_challenge"] == pkce.challenge)
        #expect(items["code_challenge_method"] == "S256")
        #expect(items["state"] == "the-state")
        #expect(items["scope"] == "openid profile email offline_access")
    }

    // MARK: - Helpers

    @Test func formURLEncodeEscapesAndSorts() {
        let encoded = CodexOAuth.formURLEncode(["b": "two words", "a": "x/y"])
        #expect(encoded == "a=x%2Fy&b=two%20words")
    }

    /// Build a JWT whose payload carries the ChatGPT account claim, as OpenAI's id_token does.
    private func idToken(accountID: String) -> String {
        let header = CodexOAuth.base64URL(Data(#"{"alg":"none"}"#.utf8))
        let payloadObj: [String: Any] = [
            "https://api.openai.com/auth": ["chatgpt_account_id": accountID],
        ]
        let payloadData = try! JSONSerialization.data(withJSONObject: payloadObj)
        let payload = CodexOAuth.base64URL(payloadData)
        return "\(header).\(payload).sig"
    }

    @Test func accountIDExtractedFromIDTokenClaim() {
        let token = idToken(accountID: "acct_9xy")
        #expect(CodexOAuth.accountID(fromIDToken: token) == "acct_9xy")
        #expect(CodexOAuth.accountID(fromIDToken: "not.a.jwt") == nil)
    }

    // MARK: - auth.json shaping

    private func tokenResponse(
        access: String = "acc", refresh: String? = "ref", account: String = "acct_1"
    ) -> Data {
        var obj: [String: Any] = ["access_token": access, "id_token": idToken(accountID: account)]
        if let refresh { obj["refresh_token"] = refresh }
        return try! JSONSerialization.data(withJSONObject: obj)
    }

    @Test func authFileHasCodexShapeAndAccountID() throws {
        let now = Date(timeIntervalSince1970: 1_700_000_000)
        let json = try #require(CodexOAuth.authFileJSON(
            fromTokenResponse: tokenResponse(), now: now))
        // Parses through the accessors the sync path uses.
        #expect(CodexAuthFile.isValidLogin(json))
        #expect(CodexAuthFile.lastRefresh(json) != nil)
        let obj = try #require(
            try JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any])
        let tokens = try #require(obj["tokens"] as? [String: Any])
        #expect(tokens["refresh_token"] as? String == "ref")
        #expect(tokens["account_id"] as? String == "acct_1")
        #expect(obj["OPENAI_API_KEY"] is NSNull)
    }

    @Test func refreshResponseWithoutRefreshTokenReusesPriorLineage() throws {
        let json = try #require(CodexOAuth.authFileJSON(
            fromTokenResponse: tokenResponse(refresh: nil), now: Date(),
            fallbackRefreshToken: "kept"))
        let obj = try JSONSerialization.jsonObject(with: Data(json.utf8)) as! [String: Any]
        let tokens = obj["tokens"] as! [String: Any]
        #expect(tokens["refresh_token"] as? String == "kept")
    }

    @Test func authFileRejectsMissingAccessIDOrRefresh() {
        // Missing id_token.
        #expect(CodexOAuth.authFileJSON(
            fromTokenResponse: Data(#"{"access_token":"a","refresh_token":"r"}"#.utf8),
            now: Date()) == nil)
        // Missing refresh with no fallback.
        #expect(CodexOAuth.authFileJSON(
            fromTokenResponse: tokenResponse(refresh: nil), now: Date()) == nil)
    }

    // MARK: - Exchange / refresh over an injected transport

    private func oauth(status: Int = 200, body: Data, now: Date = Date()) -> CodexOAuth {
        CodexOAuth(transport: { request in
            #expect(request.httpMethod == "POST")
            #expect(request.url?.absoluteString == CodexOAuth.tokenEndpoint)
            #expect(request.value(forHTTPHeaderField: "Content-Type")
                == "application/x-www-form-urlencoded")
            let response = HTTPURLResponse(
                url: request.url!, statusCode: status, httpVersion: nil, headerFields: nil)!
            return (body, response)
        }, now: { now })
    }

    @Test func exchangeProducesAuthFile() async throws {
        let client = oauth(body: tokenResponse(access: "live", account: "acct_live"))
        let pkce = CodexOAuth.PKCE(verifier: "v", state: "s")
        let json = try await client.exchange(code: "the-code", pkce: pkce)
        #expect(CodexAuthFile.isValidLogin(json))
        let tokens = (try JSONSerialization.jsonObject(with: Data(json.utf8)) as! [String: Any])["tokens"] as! [String: Any]
        #expect(tokens["access_token"] as? String == "live")
        #expect(tokens["account_id"] as? String == "acct_live")
    }

    @Test func exchangeSurfacesHTTPFailure() async {
        let client = oauth(status: 400, body: Data("bad".utf8))
        let pkce = CodexOAuth.PKCE(verifier: "v", state: "s")
        await #expect(throws: CodexOAuth.OAuthError.self) {
            _ = try await client.exchange(code: "c", pkce: pkce)
        }
    }

    @Test func refreshReturnsNilOnRejection() async {
        let client = oauth(status: 401, body: Data("revoked".utf8))
        #expect(await client.refresh(refreshToken: "r") == nil)
    }

    @Test func refreshRotatesCredential() async throws {
        let client = oauth(body: tokenResponse(access: "rot", refresh: "new-ref"))
        let json = try #require(await client.refresh(refreshToken: "old"))
        let tokens = (try JSONSerialization.jsonObject(with: Data(json.utf8)) as! [String: Any])["tokens"] as! [String: Any]
        #expect(tokens["refresh_token"] as? String == "new-ref")
    }
}
