import XCTest
@testable import AIFabric

final class LocalMLXProviderTests: XCTestCase {

    private let baseURL = URL(string: "http://100.76.254.116:8890")!

    override func tearDown() {
        MockURLProtocol.reset()
        super.tearDown()
    }

    private func provider() -> LocalMLXProvider {
        LocalMLXProvider(baseURL: baseURL, model: "mac-mini-default", session: MockURLProtocol.session())
    }

    // MARK: isAvailable probe (wake 미트리거)

    func testProbeAvailable() async throws {
        MockURLProtocol.handler = { req in
            MockURLProtocol.ok(req.url!, json: Data(#"{"data":[{"id":"gemma-macmini"}]}"#.utf8))
        }
        let available = await provider().isAvailable
        XCTAssertTrue(available)
        // probe 는 GET /v1/models
        XCTAssertEqual(MockURLProtocol.recorder.lastURL?.path, "/v1/models")
        XCTAssertEqual(MockURLProtocol.recorder.lastMethod, "GET")
    }

    func testProbeUnavailableOnError() async throws {
        MockURLProtocol.handler = { _ in throw URLError(.cannotConnectToHost) }
        let available = await provider().isAvailable
        XCTAssertFalse(available)   // 연결오류 → false(throw 아님)
    }

    func testProbeUnavailableOn500() async throws {
        MockURLProtocol.handler = { req in MockURLProtocol.status(req.url!, 500) }
        let available = await provider().isAvailable
        XCTAssertFalse(available)
    }

    // MARK: complete — 응답 매핑 + 요청 call-shape 락

    func testCompleteMapsResponseFixture() async throws {
        let body = try Fixture.data("llm-router-chat.response.json")
        MockURLProtocol.handler = { req in MockURLProtocol.ok(req.url!, json: body) }

        let resp = try await provider().complete(
            AICompletionRequest(task: .quickSummarize, prompt: "충격시험 면제 기준을 한 문장으로 요약해줘.",
                                systemPrompt: "You are a concise technical assistant.", maxTokens: 512)
        )
        XCTAssertEqual(resp.providerUsed, .localMLX)
        XCTAssertEqual(resp.finishReason, .completed)
        XCTAssertTrue(resp.citations.isEmpty)
        XCTAssertNotNil(resp.latencyMs)
        XCTAssertTrue(resp.text.contains("면제"))   // 실측 응답 본문
    }

    func testCompleteRequestCallShape() async throws {
        let body = try Fixture.data("llm-router-chat.response.json")
        MockURLProtocol.handler = { req in MockURLProtocol.ok(req.url!, json: body) }

        _ = try await provider().complete(
            AICompletionRequest(task: .quickSummarize, prompt: "PROMPT_X",
                                systemPrompt: "SYS_Y", maxTokens: 512)
        )
        // POST /v1/chat/completions
        XCTAssertEqual(MockURLProtocol.recorder.lastURL?.path, "/v1/chat/completions")
        XCTAssertEqual(MockURLProtocol.recorder.lastMethod, "POST")

        // messages system/user 분리 call-shape (load-bearing)
        let sent = try XCTUnwrap(MockURLProtocol.recorder.lastBody)
        let decoded = try JSONDecoder().decode(SentRequest.self, from: sent)
        XCTAssertEqual(decoded.model, "mac-mini-default")
        XCTAssertEqual(decoded.maxTokens, 512)
        XCTAssertEqual(decoded.stream, false)
        XCTAssertEqual(decoded.messages.count, 2)
        XCTAssertEqual(decoded.messages[0].role, "system")
        XCTAssertEqual(decoded.messages[0].content, "SYS_Y")
        XCTAssertEqual(decoded.messages[1].role, "user")
        XCTAssertEqual(decoded.messages[1].content, "PROMPT_X")
    }

    func testNilSystemPromptSendsEmptySystemMessage() async throws {
        let body = try Fixture.data("llm-router-chat.response.json")
        MockURLProtocol.handler = { req in MockURLProtocol.ok(req.url!, json: body) }
        _ = try await provider().complete(AICompletionRequest(task: .quickSummarize, prompt: "P"))
        let sent = try XCTUnwrap(MockURLProtocol.recorder.lastBody)
        let decoded = try JSONDecoder().decode(SentRequest.self, from: sent)
        XCTAssertEqual(decoded.messages[0].role, "system")
        XCTAssertEqual(decoded.messages[0].content, "")   // plan S2-2c: systemPrompt ?? ""
    }

    func testNon200BackendError() async throws {
        MockURLProtocol.handler = { req in MockURLProtocol.status(req.url!, 503, body: "model loading") }
        do {
            _ = try await provider().complete(AICompletionRequest(task: .quickSummarize, prompt: "P"))
            XCTFail("non-200 must throw backendError, not silent empty text")
        } catch let AIProviderError.backendError(id, status, reason) {
            XCTAssertEqual(id, .localMLX)
            XCTAssertEqual(status, 503)
            XCTAssertEqual(reason, "model loading")
        }
    }

    func testRequestFixtureMatchesEncoder() throws {
        // 동결 request fixture 의 call-shape 가 encodeRequest 산출과 일치(템플릿 placeholder 회귀 방지).
        let fixtureData = try Fixture.data("llm-router-chat.request.json")
        let fixture = try JSONDecoder().decode(SentRequest.self, from: fixtureData)
        XCTAssertEqual(fixture.messages.count, 2)
        XCTAssertEqual(fixture.messages[0].role, "system")
        XCTAssertEqual(fixture.messages[1].role, "user")
        XCTAssertEqual(fixture.stream, false)
    }

    // MARK: rule-fallback 라우팅 (S2-2d) — onDevice 불가 → localMLX

    func testFallbackFromOnDeviceToLocalMLX() async throws {
        let body = try Fixture.data("llm-router-chat.response.json")
        MockURLProtocol.handler = { req in MockURLProtocol.ok(req.url!, json: body) }
        let router = AIRouter(providers: [
            .onDevice: MockAIProvider(id: .onDevice, available: false),   // 불가
            .localMLX: provider(),
        ])
        let resp = try await router.route(AICompletionRequest(task: .quickSummarize, prompt: "P"))
        XCTAssertEqual(resp.providerUsed, .localMLX)
        XCTAssertEqual(resp.routingNote, "fallback from onDevice → localMLX")
    }

    func testNoFallbackNoteOnFirstChoiceSuccess() async throws {
        let body = try Fixture.data("llm-router-chat.response.json")
        MockURLProtocol.handler = { req in MockURLProtocol.ok(req.url!, json: body) }
        // classify 체인 = [.localMLX, .remoteDS, .onDevice] → 1순위 localMLX 성공 → note nil
        let router = AIRouter(providers: [.localMLX: provider()])
        let resp = try await router.route(AICompletionRequest(task: .classify, prompt: "P"))
        XCTAssertEqual(resp.providerUsed, .localMLX)
        XCTAssertNil(resp.routingNote)
    }

    // MARK: 라이브 통합 (맥미니 llm-router :8890 — offline 시 skip)

    func testLiveLocalMLXIfReachable() async throws {
        let live = LocalMLXProvider(baseURL: URL(string: "http://100.76.254.116:8890")!)  // 실 URLSession, Tailscale
        let reachable = await live.isAvailable
        guard reachable else {
            throw XCTSkip("llm-router :8890 도달 불가(맥미니 offline) — 라이브 테스트 skip")
        }
        let resp = try await live.complete(
            AICompletionRequest(task: .quickSummarize,
                                prompt: "엘보 내경 가공 핵심을 한 문장으로 요약해줘.",
                                systemPrompt: "You are a concise technical assistant.",
                                maxTokens: 200)
        )
        XCTAssertEqual(resp.providerUsed, .localMLX)
        XCTAssertEqual(resp.finishReason, .completed)
        XCTAssertFalse(resp.text.isEmpty, "라이브 응답은 비어있지 않아야")
        XCTAssertNotNil(resp.latencyMs)
    }

    /// 나간 요청 디코딩용(테스트 전용 미러).
    struct SentRequest: Decodable {
        struct Message: Decodable { let role: String; let content: String }
        let model: String
        let messages: [Message]
        let maxTokens: Int?
        let stream: Bool
        enum CodingKeys: String, CodingKey { case model, messages, stream; case maxTokens = "max_tokens" }
    }
}
