import Foundation
import Testing
@testable import ClientTLS

// C-iOS · The keychain-replace helper backs the rotation scheduler's identity
// updates. Its one job: a crash/failure mid-write must NEVER leave zero installed
// items (which would lock the device out of its own mTLS identity until a manual
// re-enroll). It enforces ADD-new-BEFORE-DELETE-old ordering — the inverse of the
// delete-then-add sequence whose window has zero items installed.

/// A tiny in-memory stand-in for the keychain: the current set of installed item
/// ids plus the smallest count ever observed, so a test can assert the invariant
/// that the installed set is NEVER empty at any step of the replace.
private final class FakeKeychain {
    private(set) var items: Set<String>
    private(set) var log: [String] = []
    private(set) var minCount: Int
    init(seed: Set<String>) {
        items = seed
        minCount = seed.count
    }

    func add(_ id: String) {
        items.insert(id)
        log.append("add(\(id))")
        minCount = min(minCount, items.count)
    }

    func delete(_ id: String) {
        items.remove(id)
        log.append("delete(\(id))")
        minCount = min(minCount, items.count)
    }
}

@Test("replace ADDS the new item before DELETING the old — never a zero-item window")
func replaceAddsBeforeDeletes() throws {
    let keychain = FakeKeychain(seed: ["old"])
    try replaceKeychainItemAtomically(
        addNew: { keychain.add("new") },
        deleteOld: { keychain.delete("old") }
    )
    #expect(keychain.items == ["new"])
    #expect(keychain.log == ["add(new)", "delete(old)"]) // add strictly precedes delete
    #expect(keychain.minCount >= 1) // at least one identity installed at every step
}

@Test("a failed add leaves the prior item intact and never runs the delete")
func replaceAddFailureKeepsOld() {
    struct Boom: Error {}
    let keychain = FakeKeychain(seed: ["old"])
    #expect(throws: Boom.self) {
        try replaceKeychainItemAtomically(
            addNew: { throw Boom() },
            deleteOld: { keychain.delete("old") }
        )
    }
    #expect(keychain.items == ["old"]) // old preserved — a working identity remains
    #expect(keychain.log.contains("delete(old)") == false)
    #expect(keychain.minCount >= 1)
}

@Test("a failed delete-of-old is non-fatal — the new item stays installed and the fault is surfaced")
func replaceDeleteFailureKeepsNew() throws {
    struct Boom: Error {}
    let keychain = FakeKeychain(seed: ["old"])
    var reported: Error?
    try replaceKeychainItemAtomically(
        addNew: { keychain.add("new") },
        deleteOld: { throw Boom() },
        reportStaleDeleteFailure: { reported = $0 }
    )
    #expect(keychain.items.contains("new")) // renewal succeeded despite the stale-delete fault
    #expect(reported is Boom) // surfaced (not silently swallowed)
    #expect(keychain.minCount >= 1)
}
