// Swift smoke test that links against the macOS slice of the
// `CeraFFI.xcframework` (or the equivalent `aarch64-apple-darwin`
// staticlib in dev) and exercises a few cross-language calls.
//
// Why macOS only: the Rust FFI surface is identical across iOS
// device, iOS simulator, and native macOS — same generated Swift
// binding, same C ABI, same staticlib code path. Validating macOS
// gives us "the entire pipeline links and a Swift call can reach
// Rust and back" without paying for an iOS simulator boot in CI.
// If a regression broke the FFI on iOS specifically (e.g., a
// platform-conditional staticlib symbol), this test wouldn't catch
// it — but those have not been a real failure mode for UniFFI 0.31.
//
// Coverage:
//   1. `ceraFfiVersion()` — minimal function call. Proves symbol
//      resolution + UniFFI scaffolding initialization.
//   2. `BackendPreference.cpu` / `.metal` — enum marshaling.
//   3. `EngineConfig(...)` — record (struct) marshaling.
//   4. `FfiError` — error-type round-trip via a contrived call
//      that we expect to fail loudly. The error message format
//      must match `cera::CeraError`'s Display per the parity test
//      shipped in PR 7.
//
// Out of scope: actually loading a model. That'd need a GGUF on
// disk + the `remote` feature or test fixtures — separate scope.

import Foundation

// Compile-time link to the UniFFI binding via `-import-objc-header
// cera_ffiFFI.h`. The Swift binding's `#if canImport(cera_ffiFFI)`
// branch is false in this single-binary swiftc invocation; the C
// symbols are resolved through the bridging header instead.

func fail(_ msg: String) -> Never {
    FileHandle.standardError.write(Data("FAIL: \(msg)\n".utf8))
    exit(1)
}

// 1. Function call → version string.
let version = ceraFfiVersion()
guard !version.isEmpty else { fail("ceraFfiVersion() returned empty") }
print("OK: cera_ffi v\(version)")

// 2. Enum marshaling — construct each variant + Swift's switch
// exhaustiveness check guards against future variant adds breaking
// silently.
let backends: [BackendPreference] = [.auto, .cpu, .gpu, .metal]
for b in backends {
    switch b {
    case .auto: print("OK: BackendPreference.auto")
    case .cpu: print("OK: BackendPreference.cpu")
    case .gpu: print("OK: BackendPreference.gpu")
    case .metal: print("OK: BackendPreference.metal")
    }
}

// 3. Record construction. EngineConfig is a Swift struct generated
// from the UniFFI Record. Setting context_size = 0 is the FFI
// sentinel for "use the model's default max_seq_len" (per the
// docstring on `EngineConfig::context_size`). `bundleRepo: nil`
// means pure-local workflow (no remote-URL resolution); PR 10 also
// exercises the `BundleRepo` constructor + a non-nil attachment
// below.
let config = EngineConfig(contextSize: 0, backend: .cpu, bundleRepo: nil)
guard config.contextSize == 0 else { fail("EngineConfig.contextSize round-trip") }
guard config.backend == .cpu else { fail("EngineConfig.backend round-trip") }
guard config.bundleRepo == nil else { fail("EngineConfig.bundleRepo nil round-trip") }
print("OK: EngineConfig(contextSize: 0, backend: .cpu, bundleRepo: nil)")

// 3b. BundleRepo construction + attachment to an EngineConfig.
// Verifies the new UniFFI Object surface from PR 10. Directory
// creation is lazy so /tmp/cera-ffi-swift-smoke-bundles doesn't need
// to exist beforehand, and the BundleRepo doesn't touch the disk
// here (first touch is the first download, which we don't trigger).
let repo = BundleRepo(storeDir: "/tmp/cera-ffi-swift-smoke-bundles")
guard repo.storeDir() == "/tmp/cera-ffi-swift-smoke-bundles" else {
    fail("BundleRepo.storeDir round-trip")
}
let configWithRepo = EngineConfig(contextSize: 0, backend: .cpu, bundleRepo: repo)
guard configWithRepo.bundleRepo != nil else { fail("EngineConfig.bundleRepo attach") }
print("OK: BundleRepo + EngineConfig.bundleRepo attach")

// 3b'. BundleRepo cache management (PR 14). cacheSize on a fresh
// repo (no downloads yet) is 0; clearCache on the same is a no-op
// success. Both methods round-trip through the FFI to cera-core.
guard try repo.cacheSize() == 0 else {
    fail("BundleRepo.cacheSize on fresh repo must be 0")
}
try repo.clearCache()
guard try repo.cacheSize() == 0 else {
    fail("BundleRepo.cacheSize after clearCache must remain 0")
}
print("OK: BundleRepo.cacheSize + clearCache (no downloads, no-op)")

// 3c. BundleRepo.withProgress + DownloadProgressSink foreign trait
// (PR 12). We don't trigger an actual download (would need network
// + a real bundle), but constructing the repo + attaching the sink
// proves the foreign-trait scaffolding wires correctly. The sink
// itself isn't invoked here — coverage of the in-flight callback
// path is in cera-core's `progressing_writer_throttles_callback`
// unit test.
// `nonisolated(unsafe)` on the mutable storage silences a Swift 6
// Sendable warning. UniFFI marks foreign-trait objects as Sendable
// (Send + Sync on the Rust side); a real consumer would put the
// recording behind a lock or actor. This smoke test never invokes
// the callback (it only verifies wiring), so unchecked is fine.
final class CountingProgressSink: DownloadProgressSink {
    nonisolated(unsafe) var calls: [(String, UInt64, UInt64?)] = []
    func onProgress(url: String, bytesDownloaded: UInt64, totalBytes: UInt64?) {
        calls.append((url, bytesDownloaded, totalBytes))
    }
}
let sink = CountingProgressSink()
let repoWithProgress = BundleRepo.withProgress(
    storeDir: "/tmp/cera-ffi-swift-smoke-bundles-progress",
    progress: sink
)
guard repoWithProgress.storeDir() == "/tmp/cera-ffi-swift-smoke-bundles-progress" else {
    fail("BundleRepo.withProgress storeDir round-trip")
}
guard sink.calls.isEmpty else {
    fail("sink should not have fired without an actual download")
}
print("OK: BundleRepo.withProgress + DownloadProgressSink wiring")

// 4. Error type — try a constructor that we know will fail and
// confirm we get a typed `FfiError` back, not a panic / abort.
// `CeraEngine.fromPath` on a nonexistent path should surface as
// `FfiError.io` (the `io::Error` underneath comes from open()).
do {
    _ = try CeraEngine.fromPath(
        path: "/no/such/path/__swift_smoke_test__",
        config: config
    )
    fail("CeraEngine.fromPath on bogus path unexpectedly succeeded")
} catch let err as FfiError {
    // Expected. The wrapper currently reports `Backend(detail: ...)`
    // for this input because `CeraEngine::from_path` validates the
    // path shape (.gguf / .json / dir) before any io::Error can
    // surface. The test is deliberately permissive — any `FfiError`
    // variant is acceptable; the goal is "the typed error
    // round-tripped through Swift at all", not a specific variant
    // or message format. Variant-level assertions belong in Rust
    // unit tests (see `cera-ffi/src/lib.rs`), not here.
    let msg = String(describing: err)
    print("OK: FfiError caught (\(msg))")
} catch {
    fail("unexpected non-FfiError thrown: \(error)")
}

print("OK: all swift smoke tests passed")
