import XCTest
@testable import ConsentOSCore

final class TCFStringEncoderTests: XCTestCase {

    // MARK: - Encoding Returns nil Before Interaction

    func test_encode_returnsNil_whenStateHasNoInteraction() {
        let state = ConsentState(visitorId: "v1") // consentedAt is nil
        let config = makeSampleConfig()
        XCTAssertNil(TCFStringEncoder.encode(state: state, config: config))
    }

    // MARK: - Encoding Returns a Non-Empty String After Interaction

    func test_encode_returnsNonEmptyString_afterAcceptAll() {
        let state = ConsentState(visitorId: "v1").acceptingAll()
        let config = makeSampleConfig()
        let result = TCFStringEncoder.encode(state: state, config: config)
        XCTAssertNotNil(result)
        XCTAssertFalse(result!.isEmpty)
    }

    func test_encode_returnsNonEmptyString_afterRejectAll() {
        let state = ConsentState(visitorId: "v1").rejectingAll()
        let config = makeSampleConfig()
        let result = TCFStringEncoder.encode(state: state, config: config)
        XCTAssertNotNil(result)
        XCTAssertFalse(result!.isEmpty)
    }

    // MARK: - Base64url Format

    func test_encode_producesValidBase64url() {
        let state = ConsentState(visitorId: "v1").acceptingAll()
        let config = makeSampleConfig()
        let tcString = TCFStringEncoder.encode(state: state, config: config)!

        // Base64url must not contain standard Base64 characters that were replaced
        XCTAssertFalse(tcString.contains("+"), "TC string must not contain '+'")
        XCTAssertFalse(tcString.contains("/"), "TC string must not contain '/'")
        XCTAssertFalse(tcString.contains("="), "TC string must not contain padding '='")
    }

    func test_encode_producesDecodableBase64() {
        let state = ConsentState(visitorId: "v1").acceptingAll()
        let config = makeSampleConfig()
        let tcString = TCFStringEncoder.encode(state: state, config: config)!

        // Convert back to standard Base64 for decoding
        var base64 = tcString
            .replacingOccurrences(of: "-", with: "+")
            .replacingOccurrences(of: "_", with: "/")
        // Pad to multiple of 4
        let remainder = base64.count % 4
        if remainder != 0 {
            base64 += String(repeating: "=", count: 4 - remainder)
        }

        let data = Data(base64Encoded: base64)
        XCTAssertNotNil(data, "TC string should be valid Base64")
        XCTAssertGreaterThan(data!.count, 0)
    }

    // MARK: - Determinism

    func test_encode_producesConsistentOutput_forSameInput() {
        // The encoder uses a fixed timestamp, so two calls with the same state
        // should produce strings of the same length (timestamps differ slightly).
        let consentDate = Date(timeIntervalSince1970: 1_700_000_000)
        let state = ConsentState(
            visitorId: "v1",
            accepted: [.analytics, .marketing],
            rejected: [.functional, .personalisation],
            consentedAt: consentDate,
            bannerVersion: "v1"
        )
        let config = makeSampleConfig()

        let result1 = TCFStringEncoder.encode(state: state, config: config)
        let result2 = TCFStringEncoder.encode(state: state, config: config)

        XCTAssertEqual(result1, result2)
    }

    // MARK: - Different States Produce Different Strings

    func test_encode_producesDistinctStrings_forAcceptAllVsRejectAll() {
        let consentDate = Date(timeIntervalSince1970: 1_700_000_000)
        let acceptedState = ConsentState(
            visitorId: "v1",
            accepted: Set(ConsentCategory.allCases.filter { $0.requiresConsent }),
            rejected: [],
            consentedAt: consentDate,
            bannerVersion: nil
        )
        let rejectedState = ConsentState(
            visitorId: "v1",
            accepted: [],
            rejected: Set(ConsentCategory.allCases.filter { $0.requiresConsent }),
            consentedAt: consentDate,
            bannerVersion: nil
        )
        let config = makeSampleConfig()

        let tcAccepted = TCFStringEncoder.encode(state: acceptedState, config: config)
        let tcRejected = TCFStringEncoder.encode(state: rejectedState, config: config)

        XCTAssertNotEqual(tcAccepted, tcRejected)
    }

    // MARK: - Minimum Length

    func test_encode_producesStringOfReasonableLength() {
        let state = ConsentState(visitorId: "v1").acceptingAll()
        let config = makeSampleConfig()
        let tcString = TCFStringEncoder.encode(state: state, config: config)!

        // A valid core TC string serialises to at least ~20 bytes before Base64url encoding
        // (the first 6 bits alone carry the version). 28 chars is a reasonable lower bound.
        XCTAssertGreaterThan(tcString.count, 28, "TC string appears too short")
    }

    // MARK: - Helpers

    private func makeSampleConfig() -> ConsentConfig {
        ConsentConfig(
            siteId: "site-001",
            siteName: "Test Site",
            blockingMode: .optIn,
            consentExpiryDays: 365,
            bannerVersion: "v1",
            bannerConfig: ConsentConfig.BannerConfig(),
            categories: []
        )
    }
}
