import XCTest
@testable import ConsentOSCore

// MARK: - Mock Storage

final class MockConsentStorage: ConsentStorageProtocol, @unchecked Sendable {

    var storedState: ConsentState?
    var storedCachedConfig: CachedConfig?
    var storedVisitorId: String = UUID().uuidString

    private(set) var saveStateCallCount = 0
    private(set) var clearStateCallCount = 0

    func loadState() -> ConsentState? { storedState }

    func saveState(_ state: ConsentState) {
        saveStateCallCount += 1
        storedState = state
    }

    func clearState() {
        clearStateCallCount += 1
        storedState = nil
    }

    func loadCachedConfig() -> CachedConfig? { storedCachedConfig }

    func saveCachedConfig(_ cached: CachedConfig) {
        storedCachedConfig = cached
    }

    func clearCachedConfig() {
        storedCachedConfig = nil
    }

    func visitorId() -> String { storedVisitorId }
}

// MARK: - Tests

final class ConsentOSTests: XCTestCase {

    private var sdk: ConsentOS!
    private var mockStorage: MockConsentStorage!
    private var mockAPI: MockConsentAPI!

    override func setUp() {
        super.setUp()
        mockStorage = MockConsentStorage()
        mockAPI = MockConsentAPI()
        sdk = ConsentOS(storage: mockStorage, api: mockAPI)
    }

    // MARK: - Configuration

    func test_configure_setsIsConfigured() {
        XCTAssertFalse(sdk.isConfigured)
        sdk.configure(
            siteId: "site-001",
            apiBase: URL(string: "https://api.example.com")!
        )
        XCTAssertTrue(sdk.isConfigured)
    }

    func test_configure_setsSiteId() {
        sdk.configure(siteId: "my-site", apiBase: URL(string: "https://api.example.com")!)
        XCTAssertEqual(sdk.siteId, "my-site")
    }

    func test_configure_restoresPersistedState() {
        let existing = ConsentState(
            visitorId: mockStorage.storedVisitorId,
            accepted: [.analytics],
            rejected: [],
            consentedAt: Date(),
            bannerVersion: "v1"
        )
        mockStorage.storedState = existing

        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)

        XCTAssertEqual(sdk.consentState?.accepted, [.analytics])
    }

    func test_configure_createsNewState_whenNoPersistedState() {
        mockStorage.storedState = nil
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)

        XCTAssertNotNil(sdk.consentState)
        XCTAssertFalse(sdk.consentState!.hasInteracted)
    }

    // MARK: - shouldShowBanner

    func test_shouldShowBanner_returnsTrue_whenNoInteraction() async {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        // State has no interaction (consentedAt is nil)
        let shouldShow = await sdk.shouldShowBanner()
        XCTAssertTrue(shouldShow)
    }

    func test_shouldShowBanner_returnsFalse_whenRecentConsentExists() async {
        mockAPI.configToReturn = makeSampleConfig(expiryDays: 365, bannerVersion: "v1")

        let recent = ConsentState(
            visitorId: "v1",
            accepted: [.analytics],
            rejected: [.marketing, .functional, .personalisation],
            consentedAt: Date(), // just now
            bannerVersion: "v1"
        )
        mockStorage.storedState = recent
        // Pre-load cached config so shouldShowBanner sees it
        mockStorage.storedCachedConfig = CachedConfig(
            config: makeSampleConfig(expiryDays: 365, bannerVersion: "v1"),
            fetchedAt: Date()
        )

        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)

        let shouldShow = await sdk.shouldShowBanner()
        XCTAssertFalse(shouldShow)
    }

    func test_shouldShowBanner_returnsTrue_whenBannerVersionChanged() async {
        mockAPI.configToReturn = makeSampleConfig(expiryDays: 365, bannerVersion: "v2")

        let state = ConsentState(
            visitorId: "v1",
            accepted: [.analytics],
            rejected: [],
            consentedAt: Date(),
            bannerVersion: "v1" // old version
        )
        mockStorage.storedState = state
        mockStorage.storedCachedConfig = CachedConfig(
            config: makeSampleConfig(expiryDays: 365, bannerVersion: "v2"),
            fetchedAt: Date()
        )

        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)

        let shouldShow = await sdk.shouldShowBanner()
        XCTAssertTrue(shouldShow)
    }

    func test_shouldShowBanner_returnsTrue_whenConsentExpired() async {
        let expiryDays = 30
        let consentedAt = Date(timeIntervalSinceNow: -Double(expiryDays * 86_400 + 1))

        let state = ConsentState(
            visitorId: "v1",
            accepted: [.analytics],
            rejected: [],
            consentedAt: consentedAt,
            bannerVersion: "v1"
        )
        mockStorage.storedState = state
        mockStorage.storedCachedConfig = CachedConfig(
            config: makeSampleConfig(expiryDays: expiryDays, bannerVersion: "v1"),
            fetchedAt: Date()
        )

        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)

        let shouldShow = await sdk.shouldShowBanner()
        XCTAssertTrue(shouldShow)
    }

    // MARK: - acceptAll

    func test_acceptAll_updatesConsentState() async {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        await sdk.acceptAll()

        let state = sdk.consentState
        XCTAssertNotNil(state)
        XCTAssertTrue(state!.hasInteracted)
        XCTAssertFalse(state!.accepted.isEmpty)
    }

    func test_acceptAll_grantsAllOptionalCategories() async {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        await sdk.acceptAll()

        for category in ConsentCategory.allCases where category.requiresConsent {
            XCTAssertTrue(sdk.getConsentStatus(for: category), "\(category) should be granted")
        }
    }

    func test_acceptAll_persistsToStorage() async {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        await sdk.acceptAll()

        XCTAssertGreaterThan(mockStorage.saveStateCallCount, 0)
        XCTAssertNotNil(mockStorage.storedState)
    }

    func test_acceptAll_postsConsentToAPI() async {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        await sdk.acceptAll()

        XCTAssertEqual(mockAPI.postConsentCallCount, 1)
        XCTAssertEqual(mockAPI.lastPostedPayload?.platform, "ios")
    }

    // MARK: - rejectAll

    func test_rejectAll_updatesConsentState() async {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        await sdk.rejectAll()

        let state = sdk.consentState
        XCTAssertNotNil(state)
        XCTAssertTrue(state!.hasInteracted)
        XCTAssertTrue(state!.accepted.isEmpty)
    }

    func test_rejectAll_deniesAllOptionalCategories() async {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        await sdk.rejectAll()

        for category in ConsentCategory.allCases where category.requiresConsent {
            XCTAssertFalse(sdk.getConsentStatus(for: category), "\(category) should be denied")
        }
    }

    // MARK: - acceptCategories

    func test_acceptCategories_onlyGrantsSpecifiedCategories() async {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        await sdk.acceptCategories([.analytics, .functional])

        XCTAssertTrue(sdk.getConsentStatus(for: .analytics))
        XCTAssertTrue(sdk.getConsentStatus(for: .functional))
        XCTAssertFalse(sdk.getConsentStatus(for: .marketing))
        XCTAssertFalse(sdk.getConsentStatus(for: .personalisation))
    }

    // MARK: - getConsentStatus

    func test_getConsentStatus_returnsTrue_forNecessary_always() {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        XCTAssertTrue(sdk.getConsentStatus(for: .necessary))
    }

    func test_getConsentStatus_returnsFalse_forOptional_beforeInteraction() {
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        XCTAssertFalse(sdk.getConsentStatus(for: .analytics))
    }

    // MARK: - Delegate

    func test_delegate_isNotifiedAfterAcceptAll() async {
        class MockDelegate: ConsentOSDelegate {
            var didChangeCalled = false
            var receivedState: ConsentState?

            func consentDidChange(_ state: ConsentState) {
                didChangeCalled = true
                receivedState = state
            }
        }

        let delegate = MockDelegate()
        sdk.delegate = delegate
        sdk.configure(siteId: "site-001", apiBase: URL(string: "https://api.example.com")!)
        await sdk.acceptAll()

        // Allow the MainActor dispatch to complete
        await Task.yield()
        await Task.yield()

        XCTAssertTrue(delegate.didChangeCalled)
        XCTAssertNotNil(delegate.receivedState)
    }

    // MARK: - Helpers

    private func makeSampleConfig(expiryDays: Int = 365, bannerVersion: String = "v1") -> ConsentConfig {
        ConsentConfig(
            siteId: "site-001",
            siteName: "Test",
            blockingMode: .optIn,
            consentExpiryDays: expiryDays,
            bannerVersion: bannerVersion,
            bannerConfig: ConsentConfig.BannerConfig(),
            categories: []
        )
    }
}
