import CodexBarCore
import Foundation
import Testing
@testable import CodexBar

struct AzureOpenAIUsageFetcherTests {
    private func makeContext(environment: [String: String]) -> ProviderFetchContext {
        let browserDetection = BrowserDetection(cacheTTL: 0)
        return ProviderFetchContext(
            runtime: .app,
            sourceMode: .auto,
            includeCredits: false,
            webTimeout: 1,
            webDebugDumpHTML: false,
            verbose: false,
            env: environment,
            settings: nil,
            fetcher: UsageFetcher(environment: environment),
            claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
            browserDetection: browserDetection)
    }

    @Test
    func `settings reader trims env vars and normalizes endpoint`() {
        let environment = [
            AzureOpenAISettingsReader.apiKeyEnvironmentKey: " 'azure-key' ",
            AzureOpenAISettingsReader.endpointEnvironmentKey: "my-resource.openai.azure.com",
            AzureOpenAISettingsReader.deploymentNameEnvironmentKey: " \"chat-deployment\" ",
        ]

        #expect(AzureOpenAISettingsReader.apiKey(environment: environment) == "azure-key")
        #expect(
            AzureOpenAISettingsReader.endpoint(environment: environment)?.absoluteString ==
                "https://my-resource.openai.azure.com")
        #expect(AzureOpenAISettingsReader.deploymentName(environment: environment) == "chat-deployment")
        #expect(AzureOpenAISettingsReader.apiVersion(environment: [:]) == AzureOpenAISettingsReader.defaultAPIVersion)
    }

    @Test
    func `missing deployment config returns precise provider error`() async {
        let descriptor = ProviderDescriptorRegistry.descriptor(for: .azureopenai)
        let outcome = await descriptor.fetchPlan.fetchOutcome(
            context: self.makeContext(environment: [
                AzureOpenAISettingsReader.apiKeyEnvironmentKey: "azure-key",
                AzureOpenAISettingsReader.endpointEnvironmentKey: "https://example-resource.openai.azure.com",
            ]),
            provider: .azureopenai)

        guard case let .failure(error) = outcome.result else {
            Issue.record("Expected missing deployment to fail")
            return
        }

        #expect(error as? AzureOpenAIUsageError == .missingDeploymentName)
        #expect(error.localizedDescription.contains("deployment not configured"))
        #expect(outcome.attempts.map(\.wasAvailable) == [true])
    }

    @Test
    func `invalid endpoint returns precise provider error before fetch`() async {
        let descriptor = ProviderDescriptorRegistry.descriptor(for: .azureopenai)
        let outcome = await descriptor.fetchPlan.fetchOutcome(
            context: self.makeContext(environment: [
                AzureOpenAISettingsReader.apiKeyEnvironmentKey: "AZURE_CANARY_KEY",
                AzureOpenAISettingsReader.endpointEnvironmentKey: "http://127.0.0.1:31337",
                AzureOpenAISettingsReader.deploymentNameEnvironmentKey: "canary-deployment",
            ]),
            provider: .azureopenai)

        guard case let .failure(error) = outcome.result else {
            Issue.record("Expected invalid endpoint override to fail")
            return
        }

        #expect(error as? AzureOpenAISettingsError == .invalidEndpointOverride(
            AzureOpenAISettingsReader.endpointEnvironmentKey))
        #expect(error.localizedDescription.contains("HTTPS endpoint"))
        #expect(outcome.attempts.map(\.wasAvailable) == [true])
    }

    @Test
    func `fetcher validates deployment with chat completions request`() async throws {
        let endpoint = try #require(URL(string: "https://example-resource.openai.azure.com"))
        let updatedAt = Date(timeIntervalSince1970: 1_800_000_000)
        let transport = ProviderHTTPTransportStub { request in
            #expect(request.httpMethod == "POST")
            #expect(request.url?.path == "/openai/deployments/chat-prod/chat/completions")
            #expect(request.url?.query == "api-version=2024-10-21")
            #expect(request.value(forHTTPHeaderField: "api-key") == "azure-key")
            #expect(request.value(forHTTPHeaderField: "Accept") == "application/json")
            #expect(request.value(forHTTPHeaderField: "Content-Type") == "application/json")

            let body = try #require(request.httpBody)
            let json = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any])
            #expect(json["max_tokens"] as? Int == 1)
            #expect(json["max_completion_tokens"] == nil)
            #expect(json["temperature"] == nil)
            #expect(json["reasoning_effort"] == nil)
            let messages = try #require(json["messages"] as? [[String: String]])
            #expect(messages.first?["content"] == "ping")

            let response = try HTTPURLResponse(
                url: #require(request.url),
                statusCode: 200,
                httpVersion: "HTTP/1.1",
                headerFields: ["Content-Type": "application/json"])!
            return (Data(#"{"id":"cmpl-1","model":"gpt-4o-mini"}"#.utf8), response)
        }

        let snapshot = try await AzureOpenAIUsageFetcher.fetchUsage(
            apiKey: "azure-key",
            endpoint: endpoint,
            deploymentName: "chat-prod",
            transport: transport,
            updatedAt: updatedAt)

        #expect(snapshot.endpointHost == "example-resource.openai.azure.com")
        #expect(snapshot.deploymentName == "chat-prod")
        #expect(snapshot.model == "gpt-4o-mini")
        #expect(snapshot.apiVersion == AzureOpenAISettingsReader.defaultAPIVersion)
        #expect(snapshot.updatedAt == updatedAt)

        let usage = snapshot.toUsageSnapshot()
        #expect(usage.identity?.providerID == .azureopenai)
        #expect(usage.identity?.accountOrganization == "example-resource.openai.azure.com")
        #expect(usage.identity?.loginMethod == "Deployment: chat-prod")
        #expect(usage.primary?.resetDescription == "Deployment: chat-prod · Model: gpt-4o-mini")
    }

    @Test
    func `chat completions URL preserves endpoint path and deployment escaping`() throws {
        let endpoint = try #require(URL(string: "https://proxy.example.com/base"))
        let url = try AzureOpenAIUsageFetcher._chatCompletionsURLForTesting(
            endpoint: endpoint,
            deploymentName: "chat prod",
            apiVersion: "2024-10-21")

        #expect(
            url.absoluteString ==
                "https://proxy.example.com/base/openai/deployments/chat%20prod/chat/completions?api-version=2024-10-21")
    }

    @Test
    func `chat completions URL does not duplicate openai endpoint suffix`() throws {
        let endpoint = try #require(URL(string: "https://proxy.example.com/base/openai"))
        let url = try AzureOpenAIUsageFetcher._chatCompletionsURLForTesting(
            endpoint: endpoint,
            deploymentName: "chat-prod",
            apiVersion: "2024-10-21")

        #expect(
            url.absoluteString ==
                "https://proxy.example.com/base/openai/deployments/chat-prod/chat/completions?api-version=2024-10-21")
    }

    @Test
    func `v1 API validates with OpenAI compatible path and model field`() async throws {
        let endpoint = try #require(URL(string: "https://example-resource.openai.azure.com"))
        let transport = ProviderHTTPTransportStub { request in
            #expect(request.httpMethod == "POST")
            #expect(
                request.url?.absoluteString ==
                    "https://example-resource.openai.azure.com/openai/v1/chat/completions")
            #expect(request.value(forHTTPHeaderField: "api-key") == "azure-key")

            let body = try #require(request.httpBody)
            let json = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any])
            #expect(json["model"] as? String == "chat-prod")
            let completionCap = try #require(json["max_completion_tokens"] as? Int)
            #expect(completionCap == 64)
            #expect(completionCap <= 64)
            #expect(json["max_tokens"] == nil)
            #expect(json["temperature"] == nil)
            #expect(json["reasoning_effort"] == nil)

            let response = try HTTPURLResponse(
                url: #require(request.url),
                statusCode: 200,
                httpVersion: "HTTP/1.1",
                headerFields: ["Content-Type": "application/json"])!
            return (Data(#"{"id":"cmpl-1","model":"gpt-4o-mini"}"#.utf8), response)
        }

        let snapshot = try await AzureOpenAIUsageFetcher.fetchUsage(
            apiKey: "azure-key",
            endpoint: endpoint,
            deploymentName: "chat-prod",
            apiVersion: "v1",
            transport: transport)

        #expect(snapshot.apiVersion == "v1")
        #expect(snapshot.deploymentName == "chat-prod")
        #expect(snapshot.model == "gpt-4o-mini")
    }

    @Test
    func `v1 API accepts documented openai v1 base URL`() throws {
        let endpoint = try #require(URL(string: "https://example-resource.openai.azure.com/openai/v1"))
        let url = try AzureOpenAIUsageFetcher._chatCompletionsURLForTesting(
            endpoint: endpoint,
            deploymentName: "chat-prod",
            apiVersion: "v1")

        #expect(
            url.absoluteString ==
                "https://example-resource.openai.azure.com/openai/v1/chat/completions")
    }
}

@MainActor
struct AzureOpenAIProviderAvailabilityTests {
    @Test
    func `configured invalid endpoint remains visible for actionable error`() throws {
        let suite = "AzureOpenAIProviderAvailabilityTests-\(UUID().uuidString)"
        let defaults = try #require(UserDefaults(suiteName: suite))
        defaults.removePersistentDomain(forName: suite)
        defer { defaults.removePersistentDomain(forName: suite) }

        let settings = SettingsStore(
            userDefaults: defaults,
            configStore: testConfigStore(suiteName: suite),
            zaiTokenStore: NoopZaiTokenStore(),
            syntheticTokenStore: NoopSyntheticTokenStore())
        settings.azureOpenAIAPIKey = "AZURE_CANARY_KEY"
        settings.azureOpenAIEndpoint = "http://127.0.0.1:31337"
        settings.azureOpenAIDeploymentName = "canary-deployment"

        let environment = ProviderRegistry.makeEnvironment(
            base: [:],
            provider: .azureopenai,
            settings: settings,
            tokenOverride: nil)
        let context = ProviderAvailabilityContext(
            provider: .azureopenai,
            settings: settings,
            environment: environment)

        #expect(AzureOpenAISettingsReader.endpoint(environment: environment) == nil)
        #expect(AzureOpenAIProviderImplementation().isAvailable(context: context))
    }
}

@MainActor
struct AzureOpenAIMenuDescriptorTests {
    @Test
    func `azure openai deployment detail appears in menu`() throws {
        let suite = "AzureOpenAIMenuDescriptorTests-menu"
        let defaults = try #require(UserDefaults(suiteName: suite))
        defaults.removePersistentDomain(forName: suite)

        let settings = SettingsStore(
            userDefaults: defaults,
            configStore: testConfigStore(suiteName: suite),
            zaiTokenStore: NoopZaiTokenStore(),
            syntheticTokenStore: NoopSyntheticTokenStore())
        settings.statusChecksEnabled = false

        let store = UsageStore(
            fetcher: UsageFetcher(environment: [:]),
            browserDetection: BrowserDetection(cacheTTL: 0),
            settings: settings)
        let snapshot = AzureOpenAIUsageSnapshot(
            endpointHost: "example-resource.openai.azure.com",
            deploymentName: "chat-prod",
            model: "gpt-4o-mini",
            apiVersion: "2024-10-21",
            updatedAt: Date(timeIntervalSince1970: 1_800_000_000))
        store._setSnapshotForTesting(snapshot.toUsageSnapshot(), provider: .azureopenai)

        let descriptor = MenuDescriptor.build(
            provider: .azureopenai,
            store: store,
            settings: settings,
            account: AccountInfo(email: nil, plan: nil),
            updateReady: false,
            includeContextualActions: false)
        let lines = descriptor.sections
            .flatMap(\.entries)
            .compactMap { entry -> String? in
                guard case let .text(text, _) = entry else { return nil }
                return text
            }

        #expect(lines.contains("Azure OpenAI"))
        #expect(lines.contains("Deployment: chat-prod · Model: gpt-4o-mini"))
    }
}
