//
//  APIClientTests.swift
//  suixinkanTests
//

import XCTest
@testable import suixinkan

@MainActor
/// APIClient 测试，覆盖 Envelope 解包、业务码失败和 token 注入。
final class APIClientTests: XCTestCase {
    func testSendDecodesSuccessEnvelope() async throws {
        let responseData = try TestJSON.envelope(data: AppConfigResponse(enableRegister: true))
        let session = MockURLSession(responses: [responseData])
        let client = APIClient(
            environment: .testing,
            session: session,
            appVersion: "1.0.0",
            osType: "iOS"
        )

        let config: AppConfigResponse = try await client.send(
            APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config")
        )

        XCTAssertTrue(config.enableRegister)
        XCTAssertEqual(session.requests.count, 1)
        XCTAssertEqual(session.requests[0].value(forHTTPHeaderField: "X-APP-VERSION"), "1.0.0")
        XCTAssertEqual(session.requests[0].value(forHTTPHeaderField: "X-OS-TYPE"), "iOS")
    }

    func testSendInjectsTokenHeaderWhenProviderBound() async throws {
        let responseData = try TestJSON.envelope(data: AppConfigResponse(enableRegister: false))
        let session = MockURLSession(responses: [responseData])
        let client = APIClient(environment: .testing, session: session)
        client.bindAuthTokenProvider { "test-token-abc" }

        _ = try await client.send(APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config"))

        XCTAssertEqual(session.requests[0].value(forHTTPHeaderField: "token"), "test-token-abc")
    }

    func testSendThrowsServerCodeErrorForBusinessFailure() async throws {
        let responseData = try TestJSON.errorEnvelope(code: 100001, msg: "密码错误")
        let session = MockURLSession(responses: [responseData])
        let client = APIClient(environment: .testing, session: session)

        do {
            let _: AppConfigResponse = try await client.send(
                APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config")
            )
            XCTFail("业务码失败时应抛出错误")
        } catch let error as APIError {
            guard case let .serverCode(code, message) = error else {
                return XCTFail("期望 serverCode，实际为 \(error)")
            }
            XCTAssertEqual(code, 100001)
            XCTAssertEqual(message, "密码错误")
        }
    }

    func testSendThrowsHTTPStatusError() async throws {
        let session = MockURLSession(results: [
            .success((
                Data(),
                HTTPURLResponse(
                    url: URL(string: "https://api-test.zhifly.cn/mock")!,
                    statusCode: 500,
                    httpVersion: nil,
                    headerFields: nil
                )!
            )),
        ])
        let client = APIClient(environment: .testing, session: session)

        do {
            let _: AppConfigResponse = try await client.send(
                APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config")
            )
            XCTFail("HTTP 500 时应抛出错误")
        } catch let error as APIError {
            guard case let .httpStatus(statusCode, _) = error else {
                return XCTFail("期望 httpStatus，实际为 \(error)")
            }
            XCTAssertEqual(statusCode, 500)
        }
    }

    func testTokenInvalidCodePostsSessionDidExpireNotification() async {
        let expectation = expectation(forNotification: NotificationName.sessionDidExpire, object: nil)
        let responseData = try! TestJSON.errorEnvelope(code: 180024, msg: "token 失效")
        let session = MockURLSession(responses: [responseData])
        let client = APIClient(environment: .testing, session: session)

        _ = try? await client.send(APIRequest<AppConfigResponse>(method: .get, path: "/api/app/config"))

        await fulfillment(of: [expectation], timeout: 1)
    }

    func testMultipartUploadBuildsAndroidCompatibleRequest() async throws {
        let session = MockURLSession(responses: [try TestJSON.envelope(data: EmptyPayload())])
        let client = APIClient(
            environment: .testing,
            session: session,
            appVersion: "9.0.0",
            osType: "iOS"
        )
        client.bindAuthTokenProvider { "avatar-token" }
        let bytes = Data([0x01, 0x02, 0xFE, 0xFF])

        let _: EmptyPayload = try await client.sendMultipart(
            path: "/api/yf-handset-app/photog/userinfo-update-avatar",
            fileName: "avatar.jpg",
            mimeType: "image/jpeg",
            data: bytes
        )

        let request = try XCTUnwrap(session.requests.first)
        XCTAssertEqual(request.httpMethod, "POST")
        XCTAssertEqual(request.timeoutInterval, 60)
        XCTAssertEqual(request.value(forHTTPHeaderField: "token"), "avatar-token")
        XCTAssertEqual(request.value(forHTTPHeaderField: "X-APP-VERSION"), "9.0.0")
        XCTAssertEqual(request.value(forHTTPHeaderField: "X-OS-TYPE"), "iOS")
        XCTAssertTrue(request.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("multipart/form-data; boundary=") == true)
        let body = try XCTUnwrap(request.httpBody)
        let bodyText = String(decoding: body, as: UTF8.self)
        XCTAssertTrue(bodyText.contains("name=\"file\"; filename=\"avatar.jpg\""))
        XCTAssertTrue(bodyText.contains("Content-Type: image/jpeg"))
        XCTAssertNotNil(body.range(of: bytes))
    }

    func testMultipartUploadMapsBusinessFailure() async {
        let session = MockURLSession(responses: [try! TestJSON.errorEnvelope(code: 100001, msg: "头像上传失败")])
        let client = APIClient(environment: .testing, session: session)

        do {
            let _: EmptyPayload = try await client.sendMultipart(
                path: "/api/yf-handset-app/photog/userinfo-update-avatar",
                fileName: "avatar.jpg",
                mimeType: "image/jpeg",
                data: Data([1])
            )
            XCTFail("业务失败时应抛出 serverCode")
        } catch let error as APIError {
            guard case let .serverCode(code, message) = error else {
                return XCTFail("期望 serverCode，实际为 \(error)")
            }
            XCTAssertEqual(code, 100001)
            XCTAssertEqual(message, "头像上传失败")
        } catch {
            XCTFail("期望 APIError，实际为 \(error)")
        }
    }
}
