import Foundation
import HostRegistry
import Security
import Testing
import WireProtocol

/// T-iOS-15 · Keychain-on-simulator assertion (deferred from T-iOS-7).
///
/// `swift test` binaries are unsigned → the data-protection keychain answers
/// `errSecMissingEntitlement` (-34018), so the package layer only tests the
/// store against a fake `SecItemShim`. THIS bundle runs signed inside the
/// WebTerm app host, so here the REAL `LiveSecItemShim` path is exercised
/// end-to-end and the §5.3 protection class is asserted on the stored item.
@Suite("KeychainHostStore (real keychain, signed app host)", .serialized)
struct KeychainHostStoreLiveTests {
    /// Test-only service/account — never collides with the production item.
    private static let service = "com.yaojia.webterm.host-registry.livetests"
    private static let account = "hosts-live-roundtrip"

    private static func makeHost() throws -> HostRegistry.Host {
        let url = try #require(URL(string: "http://192.168.1.9:3000"))
        let endpoint = try #require(HostEndpoint(baseURL: url))
        return HostRegistry.Host(id: UUID(), name: "live-roundtrip", endpoint: endpoint)
    }

    /// Removes the test item regardless of test outcome.
    private static func deleteTestItem() {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
            kSecUseDataProtectionKeychain as String: true,
        ]
        SecItemDelete(query as CFDictionary)
    }

    @Test("真 Keychain 往返（upsert/loadAll/remove）+ kSecAttrAccessible 属性断言")
    func realKeychainRoundtripAndProtectionClass() async throws {
        Self.deleteTestItem() // clean slate from any earlier aborted run
        defer { Self.deleteTestItem() } // never leave the test item behind

        // Arrange: REAL shim (default init), isolated service/account.
        let store = KeychainHostStore(service: Self.service, account: Self.account)
        let host = try Self.makeHost()

        // Act + Assert: upsert → visible via loadAll.
        let afterUpsert = try await store.upsert(host)
        #expect(afterUpsert == [host])
        #expect(try await store.loadAll() == [host])

        // Assert §5.3: the STORED item's protection class, straight from
        // SecItemCopyMatching attributes (not from our own spec constants).
        var result: CFTypeRef?
        let attributesQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: Self.service,
            kSecAttrAccount as String: Self.account,
            kSecUseDataProtectionKeychain as String: true,
            kSecReturnAttributes as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne,
        ]
        let status = SecItemCopyMatching(attributesQuery as CFDictionary, &result)
        #expect(status == errSecSuccess, "SecItemCopyMatching failed: \(status)")
        let attributes = try #require(result as? [String: Any])
        let accessible = try #require(attributes[kSecAttrAccessible as String] as? String)
        #expect(
            accessible == kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String,
            "stored item must be AfterFirstUnlockThisDeviceOnly (§5.3), got \(accessible)"
        )
        // §5.3: never iCloud-synced — the item must not be synchronizable.
        let synchronizable = attributes[kSecAttrSynchronizable as String] as? Bool ?? false
        #expect(!synchronizable)

        // Act + Assert: remove → empty store (item deleted).
        let afterRemove = try await store.remove(id: host.id)
        #expect(afterRemove.isEmpty)
        #expect(try await store.loadAll() == [])
    }
}
