import CodexBarCore
import Foundation
import Testing

struct ConfigValidationTests {
    @Test
    func `reports unsafe hook rule fields`() {
        let invalidRules = [
            HookRule(id: "duplicate", event: .quotaLow, provider: "unknown", threshold: 1.1, executable: "echo"),
            HookRule(
                id: "duplicate",
                event: .quotaReached,
                executable: "/bin/echo",
                timeoutSeconds: 301),
        ]
        let config = CodexBarConfig(
            providers: [ProviderConfig(id: .codex)],
            hooks: HooksConfig(enabled: true, events: invalidRules))
        let codes = Set(CodexBarConfigValidator.validate(config).map(\.code))

        #expect(codes.contains("invalid_hook_executable"))
        #expect(codes.contains("invalid_hook_provider"))
        #expect(codes.contains("invalid_hook_threshold"))
        #expect(codes.contains("invalid_hook_timeout"))
        #expect(codes.contains("duplicate_hook_id"))
    }

    @Test
    func `reports hook workload limits`() {
        let oversized = HookRule(
            id: String(repeating: "i", count: HookRule.maximumIDBytes + 1),
            event: .quotaReached,
            executable: "/bin/echo",
            arguments: Array(repeating: "x", count: HookRule.maximumArgumentCount + 1))
        let rules = Array(repeating: oversized, count: HooksConfig.maximumRuleCount + 1)
        let config = CodexBarConfig(providers: [], hooks: HooksConfig(enabled: true, events: rules))
        let codes = Set(CodexBarConfigValidator.validate(config).map(\.code))

        #expect(codes.contains("too_many_hook_rules"))
        #expect(codes.contains("invalid_hook_command_size"))
    }

    @Test
    func `fresh config defaults Alibaba Token Plan to International`() throws {
        let config = CodexBarConfig.makeDefault()
        let provider = try #require(config.providerConfig(for: .alibabatokenplan))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(provider.region == AlibabaTokenPlanAPIRegion.international.rawValue)
        #expect(!issues.contains(where: { $0.provider == .alibabatokenplan }))
    }

    @Test
    func `normalization preserves legacy Alibaba Token Plan region`() throws {
        let config = CodexBarConfig(providers: [
            ProviderConfig(id: .alibabatokenplan, region: nil),
        ]).normalized()
        let provider = try #require(config.providerConfig(for: .alibabatokenplan))

        #expect(provider.region == nil)
    }

    @Test
    func `normalization adds missing Alibaba Token Plan as China mainland`() throws {
        let config = CodexBarConfig(providers: [
            ProviderConfig(id: .codex),
        ]).normalized()
        let provider = try #require(config.providerConfig(for: .alibabatokenplan))

        #expect(provider.region == AlibabaTokenPlanAPIRegion.chinaMainland.rawValue)
    }

    @Test
    func `reports invalid Alibaba Token Plan region`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .alibabatokenplan, region: "nowhere"))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(issues.contains(where: {
            $0.provider == .alibabatokenplan && $0.code == "invalid_region"
        }))
    }

    @Test
    func `reports unsupported source`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .codex, source: .api))
        let issues = CodexBarConfigValidator.validate(config)
        #expect(issues.contains(where: { $0.code == "unsupported_source" }))
    }

    @Test
    func `accepts legacy factory cli source as compatibility alias`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .factory, source: .cli))
        let issues = CodexBarConfigValidator.validate(config)
        #expect(!issues.contains(where: {
            $0.provider == .factory && $0.code == "unsupported_source"
        }))
        #expect(FactoryProviderDescriptor.descriptor.fetchPlan.sourceModes.contains(.cli))
    }

    @Test
    func `reports missing API key when source API`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .zai, source: .api, apiKey: nil))
        let issues = CodexBarConfigValidator.validate(config)
        #expect(issues.contains(where: { $0.code == "api_key_missing" }))
    }

    @Test
    func `allows credentialless Wayfinder API source`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(
            id: .wayfinder,
            source: .api,
            enterpriseHost: "http://127.0.0.1:9191"))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(!issues.contains(where: { $0.provider == .wayfinder && $0.code == "api_key_missing" }))
    }

    @Test
    func `sub2api token accounts satisfy API credentials`() {
        let accounts = ProviderTokenAccountData(
            version: 1,
            accounts: [
                ProviderTokenAccount(
                    id: UUID(),
                    label: "Primary",
                    token: "fixture",
                    addedAt: 0,
                    lastUsed: nil),
            ],
            activeIndex: 0)
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(
            id: .sub2api,
            source: .api,
            enterpriseHost: "https://sub2api.example.com",
            tokenAccounts: accounts))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(!issues.contains(where: { $0.provider == .sub2api && $0.code == "api_key_missing" }))
    }

    @Test
    func `sub2api accepts HTTPS and loopback HTTP base URLs`() {
        for host in ["https://sub2api.example.com", "http://127.0.0.1:8080"] {
            var config = CodexBarConfig.makeDefault()
            config.setProviderConfig(ProviderConfig(
                id: .sub2api,
                source: .api,
                apiKey: "fixture",
                enterpriseHost: host))
            let invalidHostIssue = CodexBarConfigValidator.validate(config).first { issue in
                issue.provider == .sub2api && issue.code == "invalid_enterprise_host"
            }

            #expect(invalidHostIssue == nil)
        }
    }

    @Test
    func `sub2api rejects unsafe base URLs`() {
        let invalidHosts = [
            "http://sub2api.example.com",
            "https://user:pass@sub2api.example.com",
            "https://sub2api.example.com?token=secret",
            "https://sub2api.example.com#fragment",
        ]
        for host in invalidHosts {
            var config = CodexBarConfig.makeDefault()
            config.setProviderConfig(ProviderConfig(
                id: .sub2api,
                source: .api,
                apiKey: "fixture",
                enterpriseHost: host))
            let invalidHostIssue = CodexBarConfigValidator.validate(config).first { issue in
                issue.provider == .sub2api &&
                    issue.field == "enterpriseHost" &&
                    issue.code == "invalid_enterprise_host"
            }

            #expect(invalidHostIssue != nil)
        }
    }

    @Test
    func `sub2api rejects blank token accounts as API credentials`() {
        let accounts = ProviderTokenAccountData(
            version: 1,
            accounts: [
                ProviderTokenAccount(
                    id: UUID(),
                    label: "Blank",
                    token: "   ",
                    addedAt: 0,
                    lastUsed: nil),
            ],
            activeIndex: 0)
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(
            id: .sub2api,
            source: .api,
            enterpriseHost: "https://sub2api.example.com",
            tokenAccounts: accounts))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(issues.contains(where: { $0.provider == .sub2api && $0.code == "api_key_missing" }))
    }

    @Test
    func `reports invalid region`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .minimax, region: "nowhere"))
        let issues = CodexBarConfigValidator.validate(config)
        #expect(issues.contains(where: { $0.code == "invalid_region" }))
    }

    @Test
    func `warns on unsupported token accounts`() {
        let accounts = ProviderTokenAccountData(
            version: 1,
            accounts: [ProviderTokenAccount(id: UUID(), label: "a", token: "t", addedAt: 0, lastUsed: nil)],
            activeIndex: 0)
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .gemini, tokenAccounts: accounts))
        let issues = CodexBarConfigValidator.validate(config)
        #expect(issues.contains(where: { $0.code == "token_accounts_unused" }))
    }

    @Test
    func `allows ollama token accounts`() {
        let accounts = ProviderTokenAccountData(
            version: 1,
            accounts: [ProviderTokenAccount(id: UUID(), label: "a", token: "t", addedAt: 0, lastUsed: nil)],
            activeIndex: 0)
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .ollama, tokenAccounts: accounts))
        let issues = CodexBarConfigValidator.validate(config)
        #expect(!issues.contains(where: { $0.code == "token_accounts_unused" && $0.provider == .ollama }))
    }

    @Test
    func `accepts kilo extras config field`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .kilo, extrasEnabled: true))
        let issues = CodexBarConfigValidator.validate(config)
        #expect(!issues.contains(where: { $0.provider == .kilo && $0.field == "extrasEnabled" }))
    }

    @Test
    func `allows deepgram project workspace ID`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .deepgram, workspaceID: "project-123"))
        let issues = CodexBarConfigValidator.validate(config)
        #expect(!issues.contains(where: { $0.provider == .deepgram && $0.code == "workspace_unused" }))
    }

    @Test
    func `allows Azure OpenAI endpoint and deployment fields`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(
            id: .azureopenai,
            workspaceID: "chat-prod",
            enterpriseHost: "https://example-resource.openai.azure.com"))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(!issues.contains(where: { $0.provider == .azureopenai && $0.code == "workspace_unused" }))
        #expect(!issues.contains(where: { $0.provider == .azureopenai && $0.code == "enterprise_host_unused" }))
    }

    @Test
    func `allows LiteLLM endpoint`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(
            id: .litellm,
            apiKey: "sk-test",
            enterpriseHost: "https://litellm.example.com"))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(!issues.contains(where: { $0.provider == .litellm && $0.code == "enterprise_host_unused" }))
    }

    @Test
    func `allows OpenRouter endpoint`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(
            id: .openrouter,
            apiKey: "fixture",
            enterpriseHost: "https://router.example.com/api/v1"))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(!issues.contains(where: { $0.provider == .openrouter && $0.code == "enterprise_host_unused" }))
        #expect(!issues.contains(where: { $0.provider == .openrouter && $0.code == "invalid_enterprise_host" }))
    }

    @Test
    func `unsupported enterprise host warning lists every supported provider`() throws {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .gemini, enterpriseHost: "https://example.com"))
        let issue = try #require(CodexBarConfigValidator.validate(config).first(where: {
            $0.provider == .gemini && $0.code == "enterprise_host_unused"
        }))

        #expect(issue.message ==
            "enterpriseHost is set but only azureopenai, clawrouter, copilot, kimi, litellm, llmproxy, openrouter, " +
            "sub2api, and wayfinder " +
            "support enterpriseHost.")
    }

    @Test
    func `allows OpenAI API project workspace ID`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .openai, workspaceID: "proj_abc"))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(!issues.contains(where: { $0.provider == .openai && $0.code == "workspace_unused" }))
    }

    @Test
    func `allows doubao coding plan credential fields`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(
            id: .doubao,
            apiKey: "AKLT-config",
            secretKey: "sk-config",
            region: "cn-shanghai"))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(!issues.contains(where: { $0.provider == .doubao && $0.code == "secret_key_unused" }))
        #expect(!issues.contains(where: { $0.provider == .doubao && $0.code == "region_unused" }))
    }

    @Test
    func `warns when zai team token account is missing BigModel context`() {
        let accounts = ProviderTokenAccountData(
            version: 1,
            accounts: [
                ProviderTokenAccount(
                    id: UUID(),
                    label: "Team",
                    token: "token",
                    addedAt: 0,
                    lastUsed: nil,
                    usageScope: "team",
                    organizationID: "org_abc"),
            ],
            activeIndex: 0)
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .zai, tokenAccounts: accounts))
        let issues = CodexBarConfigValidator.validate(config)

        #expect(issues.contains(where: { $0.provider == .zai && $0.code == "zai_team_context_missing" }))
    }

    @Test
    func `warns on unsupported workspace ID`() {
        var config = CodexBarConfig.makeDefault()
        config.setProviderConfig(ProviderConfig(id: .gemini, workspaceID: "workspace-123"))
        let issues = CodexBarConfigValidator.validate(config)
        let issue = issues.first { $0.provider == .gemini && $0.code == "workspace_unused" }
        let expectedMessage =
            "workspaceID is set but only azureopenai, openai, opencode, opencodego, devin, deepgram, and xai " +
            "support workspaceID."
        #expect(issue?.message == expectedMessage)
    }

    @Test
    func `config store default url honors environment override`() {
        let url = CodexBarConfigStore.defaultURL(environment: [
            CodexBarConfigStore.pathEnvironmentKey: "~/tmp/codexbar-test-config.json",
        ])

        #expect(url.path.hasSuffix("/tmp/codexbar-test-config.json"))
    }

    @Test
    func `config store default url honors xdg config home`() throws {
        let fileManager = FileManager.default
        let home = try Self.makeTemporaryHome()
        defer { try? fileManager.removeItem(at: home) }
        let xdgHome = home.appendingPathComponent("custom-config", isDirectory: true)

        let url = CodexBarConfigStore.defaultURL(
            home: home,
            environment: [
                CodexBarConfigStore.xdgConfigHomeEnvironmentKey: xdgHome.path,
            ],
            fileManager: fileManager)

        #expect(url == Self.configURL(in: xdgHome))
    }

    @Test
    func `config store default url ignores relative xdg config home`() throws {
        let fileManager = FileManager.default
        let home = try Self.makeTemporaryHome()
        defer { try? fileManager.removeItem(at: home) }
        let legacy = Self.legacyConfigURL(in: home)
        try Self.touch(legacy, fileManager: fileManager)

        let url = CodexBarConfigStore.defaultURL(
            home: home,
            environment: [
                CodexBarConfigStore.xdgConfigHomeEnvironmentKey: "relative-config",
            ],
            fileManager: fileManager)

        #expect(url == legacy)
    }

    @Test
    func `config store default url creates in xdg default for new installs`() throws {
        let fileManager = FileManager.default
        let home = try Self.makeTemporaryHome()
        defer { try? fileManager.removeItem(at: home) }

        let url = CodexBarConfigStore.defaultURL(home: home, environment: [:], fileManager: fileManager)

        #expect(url == Self.configURL(in: home.appendingPathComponent(".config", isDirectory: true)))
    }

    @Test
    func `config store default url keeps existing legacy config`() throws {
        let fileManager = FileManager.default
        let home = try Self.makeTemporaryHome()
        defer { try? fileManager.removeItem(at: home) }
        let legacy = Self.legacyConfigURL(in: home)
        try Self.touch(legacy, fileManager: fileManager)

        let url = CodexBarConfigStore.defaultURL(home: home, environment: [:], fileManager: fileManager)

        #expect(url == legacy)
    }

    @Test
    func `config store default url prefers existing xdg default over legacy config`() throws {
        let fileManager = FileManager.default
        let home = try Self.makeTemporaryHome()
        defer { try? fileManager.removeItem(at: home) }
        let xdgDefault = Self.configURL(in: home.appendingPathComponent(".config", isDirectory: true))
        let legacy = Self.legacyConfigURL(in: home)
        try Self.touch(legacy, fileManager: fileManager)
        try Self.touch(xdgDefault, fileManager: fileManager)

        let url = CodexBarConfigStore.defaultURL(home: home, environment: [:], fileManager: fileManager)

        #expect(url == xdgDefault)
    }

    private static func makeTemporaryHome() throws -> URL {
        let url = FileManager.default.temporaryDirectory
            .appendingPathComponent("CodexBarConfigStoreTests-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
        return url
    }

    private static func touch(_ url: URL, fileManager: FileManager) throws {
        try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
        try Data().write(to: url)
    }

    private static func configURL(in directory: URL) -> URL {
        directory
            .appendingPathComponent("codexbar", isDirectory: true)
            .appendingPathComponent("config.json")
    }

    private static func legacyConfigURL(in home: URL) -> URL {
        home
            .appendingPathComponent(".codexbar", isDirectory: true)
            .appendingPathComponent("config.json")
    }
}
