import XCTest
@testable import AIFabric

/// 테스트용 DS client — ask.json 등 fixture 를 반환하거나 에러를 던지고, 받은 call-shape 를 기록.
actor MockDSAskClient: DSAskClient {
    let response: AskResponse?
    let error: Error?
    private(set) var lastBackend: String?
    private(set) var lastQuery: String?
    private(set) var callCount = 0

    init(response: AskResponse? = nil, error: Error? = nil) {
        self.response = response
        self.error = error
    }

    func ask(query: String, backend: String) async throws -> AskResponse {
        callCount += 1
        lastBackend = backend
        lastQuery = query
        if let error { throw error }
        return response!
    }
}

final class RemoteDSProviderTests: XCTestCase {

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

    // MARK: ask.json 디코딩 + 매핑 (call-shape 회귀)

    func testAskJsonDecodeAndMap() throws {
        let r = try askFixture()
        XCTAssertEqual(r.synthesisStatus, "completed")
        XCTAssertEqual(r.confidence, "high")
        XCTAssertEqual(r.backendUsed, "gemma-macmini")
        XCTAssertEqual(r.citations.count, 1)
        XCTAssertEqual(r.citations[0].docId, 4912)
        XCTAssertEqual(r.citations[0].n, 1)
        XCTAssertEqual(r.citations[0].sectionTitle, "2. UCS-66 면제 곡선")

        let mapped = RemoteDSProvider.map(r)
        XCTAssertEqual(mapped.providerUsed, .remoteDS)
        XCTAssertEqual(mapped.finishReason, .completed)
        XCTAssertEqual(mapped.citations.count, 1)
        XCTAssertEqual(mapped.citations[0].docId, 4912)
        XCTAssertEqual(mapped.confidence, .high)
        XCTAssertEqual(mapped.routingNote, "gemma-macmini")
        XCTAssertEqual(mapped.latencyMs, 2841.5)
        XCTAssertEqual(mapped.text, r.aiAnswer)
    }

    func testCompleteMapsFixture() async throws {
        let mock = MockDSAskClient(response: try askFixture())
        let provider = RemoteDSProvider(client: mock)
        let resp = try await provider.complete(
            AICompletionRequest(task: .corpusAsk, prompt: "충격시험은 언제 면제되나")
        )
        XCTAssertEqual(resp.providerUsed, .remoteDS)
        XCTAssertEqual(resp.citations.count, 1)
        XCTAssertEqual(resp.finishReason, .completed)
        XCTAssertEqual(resp.routingNote, "gemma-macmini")
    }

    // MARK: backend exhaustive switch call-shape (쿼리 파라미터 락)

    func testBackendCallShape_nilExplicit() async throws {
        let mock = MockDSAskClient(response: try askFixture())
        let provider = RemoteDSProvider(client: mock)
        _ = try await provider.complete(AICompletionRequest(task: .corpusAsk, prompt: "q"))
        let backend = await mock.lastBackend
        XCTAssertEqual(backend, "mac-mini-default")   // 미지정 → DS 기본
    }

    func testBackendCallShape_localMLXExplicit() async throws {
        let mock = MockDSAskClient(response: try askFixture())
        let provider = RemoteDSProvider(client: mock)
        _ = try await provider.complete(
            AICompletionRequest(task: .corpusAsk, prompt: "q", explicitProvider: .localMLX)
        )
        let backend = await mock.lastBackend
        XCTAssertEqual(backend, "gemma-macmini")
    }

    func testBackendMapPure() {
        XCTAssertEqual(RemoteDSProvider.dsBackend(for: nil), "mac-mini-default")
        XCTAssertEqual(RemoteDSProvider.dsBackend(for: .localMLX), "gemma-macmini")
        XCTAssertEqual(RemoteDSProvider.dsBackend(for: .remoteDS), "mac-mini-default")
        XCTAssertEqual(RemoteDSProvider.dsBackend(for: .onDevice), "mac-mini-default")
        XCTAssertEqual(RemoteDSProvider.dsBackend(for: .specialized), "mac-mini-default")
    }

    func testNonCorpusTaskNotImplemented() async throws {
        let mock = MockDSAskClient(response: try askFixture())
        let provider = RemoteDSProvider(client: mock)
        do {
            _ = try await provider.complete(AICompletionRequest(task: .quickSummarize, prompt: "q"))
            XCTFail("non-corpus task should not be served by RemoteDS")
        } catch let AIProviderError.notImplemented(id) {
            XCTAssertEqual(id, .remoteDS)
        }
    }

    // MARK: 라우팅 — corpusAsk 무폴백 (오프라인에서도 정답)

    func testCorpusAskRoutesToRemoteDSOnly() async throws {
        let router = AIRouter(providers: [
            .remoteDS: RemoteDSProvider(client: MockDSAskClient(response: try askFixture())),
            .onDevice: MockAIProvider(id: .onDevice, available: true),   // available 이어도 corpusAsk 폴백 금지
        ])
        let resp = try await router.route(AICompletionRequest(task: .corpusAsk, prompt: "q"))
        XCTAssertEqual(resp.providerUsed, .remoteDS)
        XCTAssertEqual(resp.citations.count, 1)
    }

    func testCorpusAskRemoteDSDown_NoLocalFallback() async throws {
        // remoteDS 불가 → 엉뚱한 온디바이스 코퍼스 답변 대신 명확한 실패(무폴백).
        struct Net: Error {}
        let router = AIRouter(providers: [
            .remoteDS: RemoteDSProvider(client: MockDSAskClient(error: Net())),
            .onDevice: MockAIProvider(id: .onDevice, available: true),
        ])
        do {
            _ = try await router.route(AICompletionRequest(task: .corpusAsk, prompt: "q"))
            XCTFail("corpusAsk must not fall back to onDevice")
        } catch is Net {
            // 기대: remoteDS 의 실패가 전파(코퍼스 체인 = [.remoteDS] only)
        }
    }

    // MARK: S2-4b — cloud 'claude-cloud' = 명시 503 (로컬 대체 응답 금지)

    func testCloud503Surfaces_NoSilentFallback() async throws {
        let err = AIProviderError.backendError(.remoteDS, status: 503, reason: "cloud backend pending activation")
        let router = AIRouter(providers: [
            .remoteDS: RemoteDSProvider(client: MockDSAskClient(error: err)),
            .onDevice: MockAIProvider(id: .onDevice, available: true),
        ])
        do {
            _ = try await router.route(
                AICompletionRequest(task: .corpusAsk, prompt: "q", explicitProvider: .remoteDS)
            )
            XCTFail("503 must surface, not fall back")
        } catch let AIProviderError.backendError(id, status, _) {
            XCTAssertEqual(id, .remoteDS)
            XCTAssertEqual(status, 503)
        }
    }
}
