import XCTest
@testable import AIFabric

/// S2-Fa(config 단일소스) + S2-Fb(컴포지션 루트) + S2-Fe(타임아웃/취소 전파).
final class CompositionTests: XCTestCase {

    // MARK: S2-Fa — 엔드포인트 단일소스 config

    func testConfigDefaults() {
        let c = AIProviderConfiguration.resolved(environment: [:])
        XCTAssertEqual(c.localMLXBaseURL.absoluteString, "http://100.76.254.116:8890")
        XCTAssertEqual(c.localMLXModel, "mac-mini-default")
        XCTAssertEqual(c.dsBaseURL.absoluteString, "https://document.hyungi.net/api")
        XCTAssertEqual(c.probeTimeout, 2)
    }

    func testConfigEnvOverride() {
        let c = AIProviderConfiguration.resolved(environment: [
            "AIFABRIC_LOCALMLX_URL": "http://127.0.0.1:9999",
            "AIFABRIC_LOCALMLX_MODEL": "test-model",
            "AIFABRIC_DS_URL": "http://100.110.63.63:8000/api",
        ])
        XCTAssertEqual(c.localMLXBaseURL.absoluteString, "http://127.0.0.1:9999")
        XCTAssertEqual(c.localMLXModel, "test-model")
        XCTAssertEqual(c.dsBaseURL.absoluteString, "http://100.110.63.63:8000/api")
    }

    // MARK: S2-Fb — 컴포지션 루트 (4 provider 전부 등록)

    func testMakeDefaultRouterRegistersAllFour() async throws {
        let client = MockDSAskClient(response: try Fixture.decode(AskResponse.self, from: "ask.json"))
        let router = makeDefaultRouter(client: client, session: MockURLProtocol.session(), log: { _ in })
        XCTAssertEqual(Set(router.providers.keys), Set(AIProviderID.allCases))
        // corpusAsk 는 와이어링된 RemoteDS 로 흘러 citations 매핑.
        let resp = try await router.route(AICompletionRequest(task: .corpusAsk, prompt: "p"))
        XCTAssertEqual(resp.providerUsed, .remoteDS)
        XCTAssertEqual(resp.citations.count, 1)
    }

    func testMakeDefaultRouterVisionFallbackVisible() async throws {
        let client = MockDSAskClient(response: try Fixture.decode(AskResponse.self, from: "ask.json"))
        let sink = LogSink()
        let router = makeDefaultRouter(client: client, session: MockURLProtocol.session(), log: { sink.append($0) })
        // specialized scaffold(불가) → onDevice. (onDevice 가용 여부는 머신 의존 — 최소한 specialized 불가 log 는 떠야)
        _ = try? await router.route(AICompletionRequest(task: .vision, prompt: "p"))
        XCTAssertTrue(sink.lines.contains { $0.contains("specialized") && $0.contains("unavailable") },
                      "specialized 불가가 침묵 아닌 log 로 가시화")
    }

    // MARK: S2-Fe — 취소 전파 (URLSession 경로 아날로그)

    func testCancellationPropagatesThroughRouter() async throws {
        let router = AIRouter(providers: [.localMLX: SleepingProvider(id: .localMLX)])
        let task = Task {
            try await router.route(AICompletionRequest(task: .classify, prompt: "p"))
        }
        try? await Task.sleep(nanoseconds: 100_000_000)
        task.cancel()
        do {
            _ = try await task.value
            XCTFail("취소된 생성은 CancellationError 전파")
        } catch is CancellationError {
            // 기대: URLSession async/Task.sleep 둘 다 취소 자동 honor. OnDevice respond() 도 협조적(S2-3a).
        }
    }
}
