import ClientTLS
import Foundation
import Testing
@testable import WebTerm

/// B3 · EnrollmentViewModel — the zero-`.p12` enrollment flow's state machine.
/// The enroll operation is injected as a closure (the flow logic itself is
/// covered headlessly in ClientTLS's DeviceEnrollmentFlowTests); these cover the
/// VM's boundary validation, success bookkeeping, and error→copy mapping —
/// including that the password never lingers after an attempt.
@MainActor
@Suite("EnrollmentViewModel")
struct EnrollmentViewModelTests {
    /// Records enroll invocations + replays a scripted result/error.
    private actor EnrollScript {
        private(set) var calls: [(password: String, subdomain: String, deviceName: String, url: URL)] = []
        private let result: Result<ClientCertificateSummary?, any Error>

        init(result: Result<ClientCertificateSummary?, any Error>) {
            self.result = result
        }

        func run(_ p: String, _ s: String, _ d: String, _ u: URL) throws -> ClientCertificateSummary? {
            calls.append((p, s, d, u))
            return try result.get()
        }

        var callCount: Int { calls.count }
        var lastPassword: String? { calls.last?.password }
    }

    /// Records persisted settings so a test can assert them without real defaults.
    private final class PersistSpy: @unchecked Sendable {
        private let lock = NSLock()
        private(set) var url: String?
        private(set) var subdomain: String?
        func persist(_ url: String, _ subdomain: String) {
            lock.withLock { self.url = url; self.subdomain = subdomain }
        }
    }

    private func makeVM(
        script: EnrollScript,
        persist: PersistSpy = PersistSpy(),
        url: String = "https://cp.terminal.yaojia.wang",
        subdomain: String = "alice",
        summary: ClientCertificateSummary? = nil
    ) -> EnrollmentViewModel {
        EnrollmentViewModel(
            controlPlaneURLText: url,
            subdomain: subdomain,
            deviceName: "Test iPhone",
            enrollOperation: { p, s, d, u in try await script.run(p, s, d, u) },
            loadSummary: { summary },
            persistSettings: { persist.persist($0, $1) }
        )
    }

    @Test("a successful enroll sets the summary, flags success, clears the password, persists settings")
    func successPath() async {
        let sum = ClientCertificateSummary(
            subjectCommonName: "Test iPhone", issuerCommonName: "webterm-device-ca", notAfter: nil
        )
        let script = EnrollScript(result: .success(sum))
        let persist = PersistSpy()
        let vm = makeVM(script: script, persist: persist, summary: sum)
        vm.password = "operator-secret"

        await vm.enroll()

        #expect(await script.callCount == 1)
        #expect(vm.didSucceed == true)
        #expect(vm.errorMessage == nil)
        #expect(vm.password == "") // never lingers
        #expect(vm.summary?.subjectCommonName == "Test iPhone")
        #expect(persist.url == "https://cp.terminal.yaojia.wang")
        #expect(persist.subdomain == "alice")
    }

    @Test("a non-https control-plane URL is rejected before any network")
    func rejectsInsecureURL() async {
        let script = EnrollScript(result: .success(nil))
        let vm = makeVM(script: script, url: "http://cp.terminal.yaojia.wang")
        vm.password = "pw"

        await vm.enroll()

        #expect(await script.callCount == 0) // zero network
        #expect(vm.errorMessage == EnrollmentCopy.errInvalidURL)
        #expect(vm.didSucceed == false)
    }

    @Test("a 403 subdomain-not-owned maps to actionable copy; password still cleared")
    func mapsSubdomainNotOwned() async {
        let script = EnrollScript(
            result: .failure(DeviceEnrollmentError.http(status: 403, code: "rejected"))
        )
        let vm = makeVM(script: script)
        vm.password = "operator-secret"

        await vm.enroll()

        #expect(vm.errorMessage == EnrollmentCopy.errSubdomainNotOwned)
        #expect(vm.didSucceed == false)
        #expect(vm.password == "") // cleared even on failure
    }

    @Test("a 401 login maps to the bad-credential copy")
    func mapsBadCredential() async {
        let script = EnrollScript(
            result: .failure(ControlPlaneLoginError.http(status: 401, code: "login rejected"))
        )
        let vm = makeVM(script: script)
        vm.password = "wrong"

        await vm.enroll()
        #expect(vm.errorMessage == EnrollmentCopy.errBadCredential)
    }

    @Test("Secure Enclave unavailable (simulator) maps to a clear message")
    func mapsNoSecureEnclave() async {
        let script = EnrollScript(
            result: .failure(SecureEnclaveKeyError.secureEnclaveUnavailable("no entitlement"))
        )
        let vm = makeVM(script: script)
        vm.password = "operator-secret"

        await vm.enroll()
        #expect(vm.errorMessage == EnrollmentCopy.errNoSecureEnclave)
    }

    @Test("canEnroll requires every field including a non-empty password")
    func canEnrollGating() {
        let script = EnrollScript(result: .success(nil))
        let vm = makeVM(script: script)
        #expect(vm.canEnroll == false) // password empty
        vm.password = "pw"
        #expect(vm.canEnroll == true)
        vm.subdomain = "   "
        #expect(vm.canEnroll == false) // whitespace-only subdomain
    }
}
