// KeychainTests.swift
// Unit tests for the Keychain wrapper.
//
// IMPORTANT: Each test uses a unique key (never the production key
// `Keychain.credentialKey`) so real app data is never touched.
// All test keys are deleted in tearDown even if the test fails.
//
// Note: Keychain tests require the iOS simulator or a real device with
// a valid entitlement.  They will fail in headless environments where
// the Security framework is unavailable.

import XCTest
@testable import piRemote

final class KeychainTests: XCTestCase {

    // MARK: - Helpers

    /// Unique per-run key prefix so parallel test runs don't collide.
    private var testKey: String { "test.keychain.\(name)" }

    override func tearDown() {
        // Always clean up the test key to avoid polluting the Keychain.
        Keychain.shared.delete(key: testKey)
        super.tearDown()
    }

    /// A minimal `SidecarCredential` for testing.
    private func makeCredential(name: String = "test-pi") -> SidecarCredential {
        SidecarCredential(
            sidecarId:      "sid-\(UUID().uuidString)",
            host:           "192.168.1.100",
            port:           7777,
            bearerToken:    "bearer-\(UUID().uuidString)",
            tlsFingerprint: "deadbeef1234",
            sidecarName:    name,
            pairedAt:       Date(timeIntervalSince1970: 1_716_000_000)
        )
    }

    // =========================================================================
    // MARK: 1. Save + load round-trip
    // =========================================================================

    func testSaveAndLoad_credential_roundTrips() throws {
        let credential = makeCredential(name: "my-pi")

        try Keychain.shared.save(credential, key: testKey)
        let loaded: SidecarCredential = try Keychain.shared.load(key: testKey)

        XCTAssertEqual(loaded.sidecarId,      credential.sidecarId)
        XCTAssertEqual(loaded.host,           credential.host)
        XCTAssertEqual(loaded.port,           credential.port)
        XCTAssertEqual(loaded.bearerToken,    credential.bearerToken)
        XCTAssertEqual(loaded.tlsFingerprint, credential.tlsFingerprint)
        XCTAssertEqual(loaded.sidecarName,    credential.sidecarName)
        // Date comparison within 1 second tolerance (JSON Date encoding).
        XCTAssertEqual(loaded.pairedAt.timeIntervalSince1970,
                       credential.pairedAt.timeIntervalSince1970,
                       accuracy: 1.0)
    }

    // =========================================================================
    // MARK: 2. Update (upsert)
    // =========================================================================

    func testSave_overwritesPrevious_onLoad() throws {
        let first  = makeCredential(name: "first-pi")
        let second = makeCredential(name: "second-pi")

        try Keychain.shared.save(first,  key: testKey)
        try Keychain.shared.save(second, key: testKey)

        let loaded: SidecarCredential = try Keychain.shared.load(key: testKey)
        XCTAssertEqual(loaded.sidecarName, "second-pi",
                       "Second save must overwrite the first")
    }

    // =========================================================================
    // MARK: 3. Missing key → notFound
    // =========================================================================

    func testLoad_missingKey_throwsNotFound() {
        let missingKey = "test.keychain.definitely-absent-\(UUID())"
        defer { Keychain.shared.delete(key: missingKey) }

        XCTAssertThrowsError(try Keychain.shared.load(key: missingKey) as SidecarCredential) { error in
            guard let keychainError = error as? KeychainError,
                  case .notFound = keychainError else {
                XCTFail("Expected KeychainError.notFound, got \(error)")
                return
            }
        }
    }

    // =========================================================================
    // MARK: 4. Delete clears entry
    // =========================================================================

    func testDelete_clearsEntry() throws {
        try Keychain.shared.save(makeCredential(), key: testKey)

        Keychain.shared.delete(key: testKey)

        XCTAssertThrowsError(try Keychain.shared.load(key: testKey) as SidecarCredential,
                             "After delete, load must throw") { error in
            guard let keychainError = error as? KeychainError,
                  case .notFound = keychainError else {
                XCTFail("Expected KeychainError.notFound after delete, got \(error)")
                return
            }
        }
    }

    func testDelete_missingKey_isNoOp() {
        // Deleting a key that doesn't exist must not crash or throw.
        Keychain.shared.delete(key: "test.keychain.never-saved-\(UUID())")
        // No assertion needed — reaching this line means no crash.
    }

    // =========================================================================
    // MARK: 5. Production key is NOT used in tests
    // =========================================================================

    func testProductionKeyIsUntouched() {
        // This test simply verifies that our test key is different from the
        // production credential key, so tests never corrupt real data.
        XCTAssertNotEqual(testKey, Keychain.credentialKey,
                          "Test key must differ from the production credential key")
    }

    // =========================================================================
    // MARK: 6. Generic save/load with a simple Codable type
    // =========================================================================

    private struct TestPayload: Codable, Equatable {
        let id:    String
        let value: Int
    }

    func testSaveAndLoad_simplePayload() throws {
        let key     = "test.keychain.payload.\(UUID())"
        let payload = TestPayload(id: "x", value: 99)
        defer { Keychain.shared.delete(key: key) }

        try Keychain.shared.save(payload, key: key)
        let loaded: TestPayload = try Keychain.shared.load(key: key)

        XCTAssertEqual(loaded, payload)
    }
}
