import XCTest
@testable import AIFabric

/// S2-Fc — AI-ROUTING.md §3 스모크 매트릭스를 mock provider(라이브 net 0)로 검증.
/// 정성 게이트(올바른 라우팅 시맨틱 + 정확한 에러 enum + 폴백 가시성). latency 는 기록만.
final class AIRouterSmokeTests: XCTestCase {

    private func askFixture() throws -> AskResponse {
        try Fixture.decode(AskResponse.self, from: "ask.json")
    }

    /// 모든 태스크가 1순위 성공하는 정상 패브릭.
    private func healthyRouter(log: @escaping @Sendable (String) -> Void = { _ in }) throws -> AIRouter {
        AIRouter(providers: [
            .onDevice: EchoProvider(id: .onDevice),
            .localMLX: EchoProvider(id: .localMLX),
            .remoteDS: RemoteDSProvider(client: MockDSAskClient(response: try askFixture())),
            .specialized: SpecializedProvider(),   // 불가 scaffold
        ], log: log)
    }

    func testQuickSummarizeToOnDevice() async throws {
        let resp = try await healthyRouter().route(AICompletionRequest(task: .quickSummarize, prompt: "p"))
        XCTAssertEqual(resp.providerUsed, .onDevice)
        XCTAssertNil(resp.routingNote)
    }

    func testCorpusAskToRemoteDSWithCitations() async throws {
        let resp = try await healthyRouter().route(AICompletionRequest(task: .corpusAsk, prompt: "p"))
        XCTAssertEqual(resp.providerUsed, .remoteDS)
        XCTAssertEqual(resp.citations.count, 1)
    }

    func testClassifyToLocalMLX() async throws {
        let resp = try await healthyRouter().route(AICompletionRequest(task: .classify, prompt: "p"))
        XCTAssertEqual(resp.providerUsed, .localMLX)
        XCTAssertNil(resp.routingNote)
    }

    func testVisionSpecializedUnavailableFallsToOnDeviceVisibly() async throws {
        let sink = LogSink()
        let resp = try await healthyRouter(log: { sink.append($0) })
            .route(AICompletionRequest(task: .vision, prompt: "p"))
        XCTAssertEqual(resp.providerUsed, .onDevice)
        XCTAssertEqual(resp.routingNote, "fallback from specialized → onDevice")
        XCTAssertTrue(sink.lines.contains { $0.contains("specialized") && $0.contains("unavailable") })
    }

    func testExplicitOnDeviceUnavailableErrorsNoFallback() async throws {
        let router = AIRouter(providers: [
            .onDevice: EchoProvider(id: .onDevice, available: false),
            .localMLX: EchoProvider(id: .localMLX, available: true),
        ])
        do {
            _ = try await router.route(AICompletionRequest(task: .quickSummarize, prompt: "p", explicitProvider: .onDevice))
            XCTFail("explicit onDevice 불가 → 에러(자동 fallback X)")
        } catch let AIRoutingError.explicitProviderUnavailable(id) {
            XCTAssertEqual(id, .onDevice)
        }
    }

    func testRuleFallbackOnDeviceDownToLocalMLXWithNote() async throws {
        let router = AIRouter(providers: [
            .onDevice: EchoProvider(id: .onDevice, available: false),
            .localMLX: EchoProvider(id: .localMLX, available: true),
        ])
        let resp = try await router.route(AICompletionRequest(task: .quickSummarize, prompt: "p"))
        XCTAssertEqual(resp.providerUsed, .localMLX)
        XCTAssertEqual(resp.routingNote, "fallback from onDevice → localMLX")
    }

    func testAllUnavailableYieldsNoProviderAvailable() async throws {
        let router = AIRouter(providers: [
            .onDevice: EchoProvider(id: .onDevice, available: false),
            .localMLX: EchoProvider(id: .localMLX, available: false),
        ])
        do {
            _ = try await router.route(AICompletionRequest(task: .quickSummarize, prompt: "p"))
            XCTFail("전부 불가 → noProviderAvailable")
        } catch let AIRoutingError.noProviderAvailable(task) {
            XCTAssertEqual(task, .quickSummarize)
        }
    }
}
