import XCTest
@testable import slicc_server

final class SecretInjectorTests: XCTestCase {

    private func makeInjector(secrets: [SecretInjector.LoadedSecret]) -> SecretInjector {
        SecretInjector(secrets: secrets)
    }

    private func makeSecret(
        name: String = "GITHUB_TOKEN",
        realValue: String = "ghp_realSecret123",
        maskedValue: String = "ghp_masked999abc",
        domains: [String] = ["api.github.com", "*.github.com"]
    ) -> SecretInjector.LoadedSecret {
        .init(name: name, realValue: realValue, maskedValue: maskedValue, domains: domains)
    }

    // MARK: - inject()

    func testInjectReplacesMatchedMaskWithRealValue() {
        let injector = makeInjector(secrets: [makeSecret()])
        let result = injector.inject(text: "Bearer ghp_masked999abc", hostname: "api.github.com")
        guard case .success(let text) = result else { return XCTFail("Expected success") }
        XCTAssertEqual(text, "Bearer ghp_realSecret123")
    }

    func testInjectLeavesTextUnchangedWhenNoMaskPresent() {
        let injector = makeInjector(secrets: [makeSecret()])
        let result = injector.inject(text: "Bearer some-other-token", hostname: "api.github.com")
        guard case .success(let text) = result else { return XCTFail("Expected success") }
        XCTAssertEqual(text, "Bearer some-other-token")
    }

    func testInjectReturnsDomainBlockedForUnauthorizedDomain() {
        let injector = makeInjector(secrets: [makeSecret()])
        let result = injector.inject(text: "Bearer ghp_masked999abc", hostname: "evil.com")
        guard case .domainBlocked(let secretName, let hostname) = result else {
            return XCTFail("Expected domainBlocked")
        }
        XCTAssertEqual(secretName, "GITHUB_TOKEN")
        XCTAssertEqual(hostname, "evil.com")
    }

    func testInjectAllowsWildcardDomain() {
        let injector = makeInjector(secrets: [makeSecret()])
        let result = injector.inject(text: "ghp_masked999abc", hostname: "uploads.github.com")
        guard case .success(let text) = result else { return XCTFail("Expected success") }
        XCTAssertEqual(text, "ghp_realSecret123")
    }

    func testInjectMultipleSecretsInSameText() {
        let injector = makeInjector(secrets: [
            makeSecret(name: "GH", realValue: "ghp_real1", maskedValue: "ghp_mask1", domains: ["api.github.com"]),
            makeSecret(name: "AI", realValue: "sk-real2", maskedValue: "sk-mask2", domains: ["api.github.com", "api.openai.com"]),
        ])
        let result = injector.inject(text: "GH=ghp_mask1 AI=sk-mask2", hostname: "api.github.com")
        guard case .success(let text) = result else { return XCTFail("Expected success") }
        XCTAssertEqual(text, "GH=ghp_real1 AI=sk-real2")
    }

    func testInjectBlocksIfAnySecretDomainFails() {
        let injector = makeInjector(secrets: [
            makeSecret(name: "GH", realValue: "ghp_real1", maskedValue: "ghp_mask1", domains: ["api.github.com"]),
            makeSecret(name: "AI", realValue: "sk-real2", maskedValue: "sk-mask2", domains: ["api.openai.com"]),
        ])
        // Text contains both masked values, but hostname only matches GH
        let result = injector.inject(text: "ghp_mask1 sk-mask2", hostname: "api.github.com")
        guard case .domainBlocked(let secretName, _) = result else {
            return XCTFail("Expected domainBlocked")
        }
        XCTAssertEqual(secretName, "AI")
    }

    // MARK: - injectBody()

    func testInjectBodyLeavesMaskedValueWhenDomainDoesNotMatch() {
        let injector = makeInjector(secrets: [makeSecret()])
        // Body contains masked value but domain doesn't match — should leave as-is
        let result = injector.injectBody(text: "conversation: ghp_masked999abc was used", hostname: "bedrock-runtime.us-west-2.amazonaws.com")
        XCTAssertTrue(result.contains("ghp_masked999abc"))
        XCTAssertFalse(result.contains("ghp_realSecret123"))
    }

    func testInjectBodyUnmasksWhenDomainMatches() {
        let injector = makeInjector(secrets: [makeSecret()])
        let result = injector.injectBody(text: "token=ghp_masked999abc", hostname: "api.github.com")
        XCTAssertFalse(result.contains("ghp_masked999abc"))
        XCTAssertTrue(result.contains("ghp_realSecret123"))
    }

    func testInjectBodyPartiallyUnmasksWhenSomeDomainsMatch() {
        let injector = makeInjector(secrets: [
            makeSecret(name: "GH", realValue: "ghp_real1", maskedValue: "ghp_mask1", domains: ["api.github.com"]),
            makeSecret(name: "AI", realValue: "sk-real2", maskedValue: "sk-mask2", domains: ["api.openai.com"]),
        ])
        // Send to api.github.com — GH should unmask, AI should stay masked
        let result = injector.injectBody(text: "ghp_mask1 sk-mask2", hostname: "api.github.com")
        XCTAssertTrue(result.contains("ghp_real1"))
        XCTAssertTrue(result.contains("sk-mask2"))
        XCTAssertFalse(result.contains("sk-real2"))
    }

    // MARK: - scrub()

    func testScrubReplacesRealValuesWithMasked() {
        let injector = makeInjector(secrets: [makeSecret()])
        let result = injector.scrub(text: "token: ghp_realSecret123")
        XCTAssertEqual(result, "token: ghp_masked999abc")
    }

    func testScrubMultipleOccurrences() {
        let injector = makeInjector(secrets: [makeSecret()])
        let result = injector.scrub(text: "ghp_realSecret123 and ghp_realSecret123")
        XCTAssertEqual(result, "ghp_masked999abc and ghp_masked999abc")
    }

    func testScrubMultipleSecrets() {
        let injector = makeInjector(secrets: [
            makeSecret(name: "A", realValue: "secret_a", maskedValue: "masked_a", domains: ["a.com"]),
            makeSecret(name: "B", realValue: "secret_b", maskedValue: "masked_b", domains: ["b.com"]),
        ])
        let result = injector.scrub(text: "A=secret_a B=secret_b")
        XCTAssertEqual(result, "A=masked_a B=masked_b")
    }

    func testScrubLeavesTextUnchangedWhenNoRealValues() {
        let injector = makeInjector(secrets: [makeSecret()])
        let result = injector.scrub(text: "nothing to scrub here")
        XCTAssertEqual(result, "nothing to scrub here")
    }

    // MARK: - isEmpty

    func testIsEmptyWithNoSecrets() {
        let injector = makeInjector(secrets: [])
        XCTAssertTrue(injector.isEmpty)
    }

    func testIsEmptyWithSecrets() {
        let injector = makeInjector(secrets: [makeSecret()])
        XCTAssertFalse(injector.isEmpty)
    }

    // MARK: - maskedEnvironment

    func testMaskedEnvironmentReturnsMaskedValues() {
        let injector = makeInjector(secrets: [
            makeSecret(name: "TOKEN_A", realValue: "real_a", maskedValue: "mask_a", domains: ["a.com"]),
            makeSecret(name: "TOKEN_B", realValue: "real_b", maskedValue: "mask_b", domains: ["b.com"]),
        ])
        let env = injector.maskedEnvironment
        XCTAssertEqual(env["TOKEN_A"], "mask_a")
        XCTAssertEqual(env["TOKEN_B"], "mask_b")
        XCTAssertEqual(env.count, 2)
    }

    // MARK: - Integration with mask() function

    func testInitWithSessionIdProducesDeterministicMasks() {
        // Use real masking to verify integration — can't use Keychain in tests
        // so just verify the mask function is called correctly
        let realValue = "ghp_testValue123"
        let sessionId = "test-session-42"
        let maskedA = mask(sessionId: sessionId, secretName: "GH", realValue: realValue)
        let maskedB = mask(sessionId: sessionId, secretName: "GH", realValue: realValue)
        XCTAssertEqual(maskedA, maskedB)
        XCTAssertNotEqual(maskedA, realValue)
        XCTAssertTrue(maskedA.hasPrefix("ghp_"))
    }

    // MARK: - unmaskAuthorizationBasic

    private func base64(_ s: String) -> String {
        Data(s.utf8).base64EncodedString()
    }

    func testBasicAuthDecodesUnmasksAndReencodesOnAllowedDomain() {
        let injector = makeInjector(secrets: [
            makeSecret(
                name: "GITHUB_TOKEN",
                realValue: "ghp_realToken123",
                maskedValue: "ghp_masked999abc",
                domains: ["github.com", "*.github.com"]
            ),
        ])
        let header = "Basic \(base64("x-access-token:ghp_masked999abc"))"
        let result = injector.unmaskAuthorizationBasic(value: header, targetHostname: "github.com")
        XCTAssertNil(result.forbidden)
        XCTAssertTrue(result.value.hasPrefix("Basic "))
        let payload = String(result.value.dropFirst("Basic ".count))
        guard let decoded = Data(base64Encoded: payload).flatMap({ String(data: $0, encoding: .utf8) }) else {
            return XCTFail("Re-encoded payload must decode")
        }
        XCTAssertEqual(decoded, "x-access-token:ghp_realToken123")
    }

    func testBasicAuthForbidsWhenDomainNotAllowed() {
        let injector = makeInjector(secrets: [
            makeSecret(
                name: "GITHUB_TOKEN",
                realValue: "ghp_realToken123",
                maskedValue: "ghp_masked999abc",
                domains: ["github.com"]
            ),
        ])
        let header = "Basic \(base64("u:ghp_masked999abc"))"
        let result = injector.unmaskAuthorizationBasic(value: header, targetHostname: "evil.example.com")
        XCTAssertNotNil(result.forbidden)
        XCTAssertEqual(result.forbidden?.secretName, "GITHUB_TOKEN")
        XCTAssertEqual(result.forbidden?.hostname, "evil.example.com")
        XCTAssertEqual(result.value, header)
    }

    func testBasicAuthLeavesUnchangedOnInvalidBase64() {
        let injector = makeInjector(secrets: [makeSecret()])
        let header = "Basic %%%not-b64%%%"
        let result = injector.unmaskAuthorizationBasic(value: header, targetHostname: "github.com")
        XCTAssertEqual(result.value, header)
        XCTAssertNil(result.forbidden)
    }

    func testBasicAuthLeavesUnchangedOnNoColon() {
        let injector = makeInjector(secrets: [makeSecret()])
        let header = "Basic \(base64("nocolon"))"
        let result = injector.unmaskAuthorizationBasic(value: header, targetHostname: "github.com")
        XCTAssertEqual(result.value, header)
        XCTAssertNil(result.forbidden)
    }

    func testBasicAuthLeavesUnchangedOnNoMask() {
        let injector = makeInjector(secrets: [makeSecret()])
        let header = "Basic \(base64("u:plain"))"
        let result = injector.unmaskAuthorizationBasic(value: header, targetHostname: "github.com")
        XCTAssertEqual(result.value, header)
        XCTAssertNil(result.forbidden)
    }

    // MARK: - extractAndUnmaskUrlCredentials

    func testUrlCredsStripsAndSynthesizesAuthHeader() {
        let injector = makeInjector(secrets: [
            makeSecret(
                name: "GITHUB_TOKEN",
                realValue: "ghp_realToken123",
                maskedValue: "ghp_masked999abc",
                domains: ["github.com"]
            ),
        ])
        let url = "https://x-access-token:ghp_masked999abc@github.com/owner/repo.git"
        let result = injector.extractAndUnmaskUrlCredentials(rawUrl: url)
        XCTAssertEqual(result.url, "https://github.com/owner/repo.git")
        XCTAssertNil(result.forbidden)
        guard let auth = result.syntheticAuthorization else {
            return XCTFail("Expected synthetic Authorization")
        }
        XCTAssertTrue(auth.hasPrefix("Basic "))
        let payload = String(auth.dropFirst("Basic ".count))
        guard let decoded = Data(base64Encoded: payload).flatMap({ String(data: $0, encoding: .utf8) }) else {
            return XCTFail("Re-encoded payload must decode")
        }
        XCTAssertEqual(decoded, "x-access-token:ghp_realToken123")
    }

    func testUrlCredsForbidsWhenHostNotAllowed() {
        let injector = makeInjector(secrets: [
            makeSecret(
                name: "GITHUB_TOKEN",
                realValue: "ghp_realToken123",
                maskedValue: "ghp_masked999abc",
                domains: ["github.com"]
            ),
        ])
        let url = "https://u:ghp_masked999abc@evil.example.com/"
        let result = injector.extractAndUnmaskUrlCredentials(rawUrl: url)
        XCTAssertEqual(result.forbidden?.secretName, "GITHUB_TOKEN")
        XCTAssertEqual(result.forbidden?.hostname, "evil.example.com")
        XCTAssertEqual(result.url, url)
        XCTAssertNil(result.syntheticAuthorization)
    }

    func testUrlCredsStripsUserInfoEvenWithoutMatch() {
        let injector = makeInjector(secrets: [makeSecret()])
        let url = "https://u:plain@github.com/"
        let result = injector.extractAndUnmaskUrlCredentials(rawUrl: url)
        XCTAssertEqual(result.url, "https://github.com/")
        XCTAssertNil(result.syntheticAuthorization)
        XCTAssertNil(result.forbidden)
    }

    func testUrlCredsLeavesUnchangedWhenNoUserInfo() {
        let injector = makeInjector(secrets: [makeSecret()])
        let url = "https://github.com/foo"
        let result = injector.extractAndUnmaskUrlCredentials(rawUrl: url)
        XCTAssertEqual(result.url, url)
        XCTAssertNil(result.syntheticAuthorization)
        XCTAssertNil(result.forbidden)
    }

    func testUrlCredsLeavesUnchangedOnMalformedUrl() {
        let injector = makeInjector(secrets: [makeSecret()])
        // Foundation's URLComponents is much more permissive than the JS URL
        // constructor — `"not a url"` parses as a relative path with no
        // userinfo. The path "no userinfo present" exits early with the URL
        // unchanged, so the resulting behavior matches the TS catch-all
        // (input returned verbatim) without needing a forced parse failure.
        let url = "not a url"
        let result = injector.extractAndUnmaskUrlCredentials(rawUrl: url)
        XCTAssertEqual(result.url, url)
        XCTAssertNil(result.syntheticAuthorization)
        XCTAssertNil(result.forbidden)
    }

    // MARK: - unmaskBodyBytes

    func testUnmaskBodyBytesReplacesMaskedInUtf8Body() {
        let injector = makeInjector(secrets: [
            makeSecret(
                name: "GITHUB_TOKEN",
                realValue: "ghp_realToken123",
                maskedValue: "ghp_masked999abc",
                domains: ["github.com"]
            ),
        ])
        let body = Data("hello ghp_masked999abc world".utf8)
        let out = injector.unmaskBodyBytes(bytes: body, targetHostname: "github.com")
        XCTAssertEqual(String(data: out, encoding: .utf8), "hello ghp_realToken123 world")
    }

    func testUnmaskBodyBytesDoesNotCorruptArbitraryBytesWhenNoMatch() {
        let injector = makeInjector(secrets: [
            makeSecret(
                name: "GITHUB_TOKEN",
                realValue: "ghp_realToken123",
                maskedValue: "ghp_masked999abc",
                domains: ["github.com"]
            ),
        ])
        let before = Data([0xff, 0xfe, 0x00, 0x01, 0xc3, 0x28, 0xa0, 0x80])
        let out = injector.unmaskBodyBytes(bytes: before, targetHostname: "github.com")
        XCTAssertEqual(out, before)
    }

    func testUnmaskBodyBytesByteAlignedReplacement() {
        let injector = makeInjector(secrets: [
            makeSecret(
                name: "GITHUB_TOKEN",
                realValue: "ghp_realToken123",
                maskedValue: "ghp_masked999abc",
                domains: ["github.com"]
            ),
        ])
        var input = Data([0xff, 0xfe, 0x00])
        input.append(Data("ghp_masked999abc".utf8))
        input.append(Data([0x01, 0xff]))
        let out = injector.unmaskBodyBytes(bytes: input, targetHostname: "github.com")
        var expected = Data([0xff, 0xfe, 0x00])
        expected.append(Data("ghp_realToken123".utf8))
        expected.append(Data([0x01, 0xff]))
        XCTAssertEqual(out, expected)
    }

    func testUnmaskBodyBytesLeavesUntouchedOnDomainMismatch() {
        let injector = makeInjector(secrets: [
            makeSecret(
                name: "GITHUB_TOKEN",
                realValue: "ghp_realToken123",
                maskedValue: "ghp_masked999abc",
                domains: ["github.com"]
            ),
        ])
        let body = Data("hello ghp_masked999abc world".utf8)
        let out = injector.unmaskBodyBytes(bytes: body, targetHostname: "evil.example.com")
        XCTAssertEqual(String(data: out, encoding: .utf8), "hello ghp_masked999abc world")
    }

    // MARK: - scrubResponseBytes

    func testScrubResponseBytesReplacesRealWithMaskedInUtf8() {
        let injector = makeInjector(secrets: [
            makeSecret(
                name: "GITHUB_TOKEN",
                realValue: "ghp_realToken123",
                maskedValue: "ghp_masked999abc",
                domains: ["github.com"]
            ),
        ])
        let body = Data("hello ghp_realToken123 world".utf8)
        let out = injector.scrubResponseBytes(bytes: body)
        XCTAssertEqual(String(data: out, encoding: .utf8), "hello ghp_masked999abc world")
    }

    func testScrubResponseBytesLeavesArbitraryNonUtf8Untouched() {
        let injector = makeInjector(secrets: [makeSecret()])
        let before = Data([0xff, 0xfe, 0x00, 0x01, 0xc3, 0x28, 0xa0, 0x80])
        let out = injector.scrubResponseBytes(bytes: before)
        XCTAssertEqual(out, before)
    }

    // MARK: - OAuth store chaining

    /// Uniquely-named secrets keep the assertion independent of any ambient
    /// Keychain entries that `SecretStore.all()` may surface during init.
    private func uniqueName(_ base: String) -> String {
        "OAUTHTEST_\(UUID().uuidString.prefix(8))_\(base)"
    }

    func testOAuthSecretUnmasksViaBearerHeaderForAllowedDomain() async throws {
        let name = uniqueName("PROVIDER_TOKEN")
        let oauth = OAuthSecretStore()
        try await oauth.set(name: name, value: "ghp_oauth_real", domains: ["api.github.com"])

        let injector = SecretInjector(
            sessionId: "test-session-oauth-1",
            envFileSecrets: [],
            oauthStore: oauth
        )
        await injector.reload()

        guard let masked = injector.maskedValue(for: name) else {
            return XCTFail("OAuth entry should be present after reload")
        }
        let header = "Bearer \(masked)"
        let result = injector.inject(text: header, hostname: "api.github.com")
        guard case .success(let text) = result else { return XCTFail("Expected success") }
        XCTAssertEqual(text, "Bearer ghp_oauth_real")
    }

    func testOAuthEntryOverridesEnvFileEntryWithSameName() async throws {
        let name = uniqueName("RESERVED_TOKEN")
        let envSecret = Secret(name: name, value: "env-file-real", domains: ["api.example.com"])
        let oauth = OAuthSecretStore()
        try await oauth.set(name: name, value: "oauth-real", domains: ["api.example.com"])

        let injector = SecretInjector(
            sessionId: "test-session-oauth-2",
            envFileSecrets: [envSecret],
            oauthStore: oauth
        )
        await injector.reload()

        // After reload, masked value should mask the OAuth real value, not the env one.
        guard let masked = injector.maskedValue(for: name) else {
            return XCTFail("Entry should exist")
        }
        // Inject masked → real should yield the OAuth real value, not env.
        let result = injector.inject(text: masked, hostname: "api.example.com")
        guard case .success(let text) = result else { return XCTFail("Expected success") }
        XCTAssertEqual(text, "oauth-real")
        XCTAssertNotEqual(text, "env-file-real")
    }
}

