import XCTest
@testable import ConsentOSCore

// MARK: - Mock API

/// In-memory API implementation for testing without network calls.
final class MockConsentAPI: ConsentAPIProtocol, @unchecked Sendable {

    // MARK: - Configurable Behaviour

    var configToReturn: ConsentConfig?
    var errorToThrow: Error?
    var postConsentError: Error?

    // MARK: - Call Tracking

    private(set) var fetchConfigCallCount = 0
    private(set) var postConsentCallCount = 0
    private(set) var lastPostedPayload: ConsentPayload?
    private(set) var lastFetchedSiteId: String?

    func fetchConfig(siteId: String) async throws -> ConsentConfig {
        fetchConfigCallCount += 1
        lastFetchedSiteId = siteId
        if let error = errorToThrow { throw error }
        guard let config = configToReturn else {
            throw ConsentAPIError.unexpectedStatusCode(404)
        }
        return config
    }

    func postConsent(_ payload: ConsentPayload) async throws {
        postConsentCallCount += 1
        lastPostedPayload = payload
        if let error = postConsentError { throw error }
    }
}

// MARK: - Tests

final class ConsentAPITests: XCTestCase {

    private var mockAPI: MockConsentAPI!
    private let siteId = "test-site-001"

    override func setUp() {
        super.setUp()
        mockAPI = MockConsentAPI()
    }

    // MARK: - fetchConfig

    func test_fetchConfig_returnsConfig_whenSuccessful() async throws {
        mockAPI.configToReturn = makeSampleConfig()

        let result = try await mockAPI.fetchConfig(siteId: siteId)

        XCTAssertEqual(result.siteId, siteId)
        XCTAssertEqual(mockAPI.fetchConfigCallCount, 1)
        XCTAssertEqual(mockAPI.lastFetchedSiteId, siteId)
    }

    func test_fetchConfig_throwsError_onNetworkFailure() async {
        mockAPI.errorToThrow = ConsentAPIError.networkFailure(
            NSError(domain: "NSURLErrorDomain", code: -1009)
        )

        do {
            _ = try await mockAPI.fetchConfig(siteId: siteId)
            XCTFail("Expected an error to be thrown")
        } catch ConsentAPIError.networkFailure {
            // Expected
        } catch {
            XCTFail("Unexpected error type: \(error)")
        }
    }

    func test_fetchConfig_throwsError_on404() async {
        mockAPI.errorToThrow = ConsentAPIError.unexpectedStatusCode(404)

        do {
            _ = try await mockAPI.fetchConfig(siteId: siteId)
            XCTFail("Expected an error to be thrown")
        } catch ConsentAPIError.unexpectedStatusCode(let code) {
            XCTAssertEqual(code, 404)
        } catch {
            XCTFail("Unexpected error type: \(error)")
        }
    }

    // MARK: - postConsent

    func test_postConsent_sendsCorrectPayload() async throws {
        let consentedAt = Date(timeIntervalSince1970: 1_700_000_000)
        let payload = ConsentPayload(
            siteId: siteId,
            visitorId: "visitor-xyz",
            accepted: [.analytics, .functional],
            rejected: [.marketing],
            consentedAt: consentedAt,
            bannerVersion: "v2",
            tcString: "test-tc-string"
        )

        try await mockAPI.postConsent(payload)

        XCTAssertEqual(mockAPI.postConsentCallCount, 1)
        let sent = try XCTUnwrap(mockAPI.lastPostedPayload)
        XCTAssertEqual(sent.siteId, siteId)
        XCTAssertEqual(sent.visitorId, "visitor-xyz")
        XCTAssertEqual(sent.platform, "ios")
        XCTAssertTrue(sent.accepted.contains("analytics"))
        XCTAssertTrue(sent.accepted.contains("functional"))
        XCTAssertTrue(sent.rejected.contains("marketing"))
        XCTAssertEqual(sent.bannerVersion, "v2")
        XCTAssertEqual(sent.tcString, "test-tc-string")
    }

    func test_postConsent_platformAlwaysIOS() async throws {
        let payload = ConsentPayload(
            siteId: siteId,
            visitorId: "v",
            accepted: [],
            rejected: [],
            consentedAt: Date(),
            bannerVersion: nil
        )

        try await mockAPI.postConsent(payload)

        XCTAssertEqual(mockAPI.lastPostedPayload?.platform, "ios")
    }

    func test_postConsent_throwsError_onFailure() async {
        mockAPI.postConsentError = ConsentAPIError.unexpectedStatusCode(500)

        let payload = ConsentPayload(
            siteId: siteId,
            visitorId: "v",
            accepted: [],
            rejected: [],
            consentedAt: Date(),
            bannerVersion: nil
        )

        do {
            try await mockAPI.postConsent(payload)
            XCTFail("Expected error")
        } catch ConsentAPIError.unexpectedStatusCode(let code) {
            XCTAssertEqual(code, 500)
        } catch {
            XCTFail("Unexpected error: \(error)")
        }
    }

    // MARK: - ConsentPayload Serialisation

    func test_consentPayload_encodesCategoriesToRawValues() throws {
        let payload = ConsentPayload(
            siteId: "s1",
            visitorId: "v1",
            accepted: [.analytics, .marketing],
            rejected: [.functional],
            consentedAt: Date(),
            bannerVersion: nil
        )

        XCTAssertTrue(payload.accepted.contains("analytics"))
        XCTAssertTrue(payload.accepted.contains("marketing"))
        XCTAssertTrue(payload.rejected.contains("functional"))
        XCTAssertFalse(payload.accepted.contains("necessary"))
    }

    // MARK: - Error Descriptions

    func test_invalidURL_hasDescription() {
        let error = ConsentAPIError.invalidURL
        XCTAssertNotNil(error.errorDescription)
        XCTAssertFalse(error.errorDescription!.isEmpty)
    }

    func test_unexpectedStatusCode_includesCodeInDescription() {
        let error = ConsentAPIError.unexpectedStatusCode(503)
        XCTAssertTrue(error.errorDescription?.contains("503") ?? false)
    }

    // MARK: - Helpers

    private func makeSampleConfig() -> ConsentConfig {
        ConsentConfig(
            siteId: siteId,
            siteName: "Test Site",
            blockingMode: .optIn,
            consentExpiryDays: 365,
            bannerVersion: "v1",
            bannerConfig: ConsentConfig.BannerConfig(),
            categories: []
        )
    }
}
