import Foundation
import Testing

@testable import NucleicCore

/// One-time, process-wide isolation guards for this test bundle. Installed by the
/// `.isolatedContainerSettings` trait before any covered test body runs.
enum TestProcessGuards {
    /// Idempotent (backed by a once-initialized static).
    static func install() { _ = installed }

    private static let installed: Void = {
        // No unit test may ever reach the user's real Claude login-Keychain item: headless the
        // ACL prompt can't be shown and `SecItemCopyMatching` blocks forever while holding
        // `ClaudeLoginKeychain.lock`, hanging the whole suite. `serviceEnabled == false` already
        // keeps tests off the sandbox-provisioning path that leads there — this is the backstop.
        ClaudeLoginKeychain.disabled = true
        // Scrub `nucleic.*` keys that a killed run of the old global-mutating tests stranded in
        // this process's standard domain (`swiftpm-testing-helper`). Tests only ever write
        // task-local isolated suites now, so anything nucleic-prefixed here is pollution — and a
        // leaked `serviceEnabled = true` would make every SessionController test believe it is
        // sandboxed.
        let defaults = UserDefaults.standard
        for key in defaults.dictionaryRepresentation().keys where key.hasPrefix("nucleic.") {
            defaults.removeObject(forKey: key)
        }
    }()
}

/// A purely in-memory UserDefaults: reads and writes live in a locked dictionary, never touching
/// cfprefsd or `~/Library/Preferences` (a real `UserDefaults(suiteName:)` leaves an empty plist
/// behind per test even after `removePersistentDomain`). Every accessor the code under test uses
/// is overridden; the convenience getters are overridden individually because Foundation does not
/// promise they funnel through `object(forKey:)`. The superclass is initialized with a unique
/// unwritten suite so even an un-overridden method can never reach the standard domain.
final class EphemeralDefaults: UserDefaults, @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: Any] = [:]

    init() { super.init(suiteName: "nucleic-ephemeral-\(UUID().uuidString)")! }

    private func get(_ key: String) -> Any? {
        lock.lock()
        defer { lock.unlock() }
        return storage[key]
    }

    private func put(_ value: Any?, _ key: String) {
        lock.lock()
        defer { lock.unlock() }
        storage[key] = value
    }

    override func object(forKey key: String) -> Any? { get(key) }
    override func string(forKey key: String) -> String? { get(key) as? String }
    override func bool(forKey key: String) -> Bool { get(key) as? Bool ?? false }
    override func integer(forKey key: String) -> Int { get(key) as? Int ?? 0 }
    override func set(_ value: Any?, forKey key: String) { put(value, key) }
    override func set(_ value: Bool, forKey key: String) { put(value, key) }
    override func set(_ value: Int, forKey key: String) { put(value, key) }
    override func removeObject(forKey key: String) { put(nil, key) }
    override func dictionaryRepresentation() -> [String: Any] {
        lock.lock()
        defer { lock.unlock() }
        return storage
    }
}

/// Runs every test in the suite with `ContainerServiceSettings.defaults` bound to a fresh,
/// in-memory store. The binding is task-local, so parallel suites can't observe each
/// other's settings and nothing touches the process-global domain — flipping
/// `nucleic.container.serviceEnabled` globally used to race concurrently running
/// SessionController tests into the real sandbox-provisioning (and login-Keychain) path.
///
/// Apply to any suite that constructs an `AppStore` or `SessionController`, or that reads/writes
/// `ContainerServiceSettings` — those tests reach the bound store as
/// `ContainerServiceSettings.defaults` and start from a clean slate every test.
struct IsolatedContainerSettingsTrait: TestTrait, SuiteTrait, TestScoping {
    var isRecursive: Bool { true }

    func provideScope(
        for test: Test, testCase: Test.Case?,
        performing function: @Sendable () async throws -> Void
    ) async throws {
        TestProcessGuards.install()
        try await ContainerServiceSettings.withDefaults(EphemeralDefaults()) {
            try await function()
        }
    }
}

extension Trait where Self == IsolatedContainerSettingsTrait {
    static var isolatedContainerSettings: Self { Self() }
}

enum Fixtures {
    static func url(_ name: String) -> URL {
        guard
            let url = Bundle.module.url(
                forResource: name, withExtension: "ndjson", subdirectory: "Fixtures/claude")
        else {
            fatalError("missing fixture \(name)")
        }
        return url
    }

    static func lines(_ name: String) throws -> [Data] {
        let data = try Data(contentsOf: url(name))
        return data.split(separator: 0x0A, omittingEmptySubsequences: true).map(Data.init)
    }

    /// Run a whole fixture through a fresh decoder, returning just the kinds.
    static func decode(_ name: String) throws -> [AgentEvent.Kind] {
        let decoder = ClaudeStreamDecoder()
        return try lines(name).flatMap { decoder.decode(line: $0).map(\.kind) }
    }
}

/// Locates executable products (fake-claude, fake-grok) for the current build.
/// Under the native SwiftPM backend Bundle.module sits directly in the products
/// directory; under the SwiftBuild backend it is nested inside the xctest bundle
/// (NucleicCoreTests.xctest/Contents/Resources/), so walk up from the bundle
/// until a directory containing the executables is found.
var productsDirectory: URL {
    var dir = Bundle.module.bundleURL.deletingLastPathComponent()
    for _ in 0..<8 {
        if FileManager.default.fileExists(atPath: dir.appendingPathComponent("fake-claude").path) {
            return dir
        }
        let parent = dir.deletingLastPathComponent()
        if parent.path == dir.path { break }
        dir = parent
    }
    return Bundle.module.bundleURL.deletingLastPathComponent()
}
