// AppStateLifecycleTests.swift
// T-2.10 — Background lifecycle: AppState surface for lifecycle events.
//
// Covers:
//   • Face-ID gate regression (appDidBackground / appWillForeground existing logic).
//   • New T-2.10 requirement: AppState must publish lifecycle transitions so
//     an active SessionConnection can react (suspend on background, resume on
//     foreground).
//
// Strategy: stub extension at the bottom provides `lifecycleTransitions` as an
// always-empty publisher. Tests expecting emissions will FAIL until the impl
// agent adds a real publisher on AppState.

import Combine
import XCTest
@testable import piRemote

@MainActor
final class AppStateLifecycleTests: XCTestCase {

    private nonisolated(unsafe) var cancellables = Set<AnyCancellable>()

    override func tearDown() {
        cancellables.removeAll()
        super.tearDown()
    }

    // =========================================================================
    // MARK: 1. Face-ID gate regression (existing logic must not regress)
    // =========================================================================

    /// Within the 60-second grace period, appWillForeground must NOT lock.
    func test_appWillForeground_withinGracePeriod_doesNotLock() async {
        let state = AppState.shared
        // Simulate a recent background event (< 60 s ago)
        state.appDidBackground()
        // Immediately foreground — elapsed ≈ 0 s, well within the 60 s window.
        await state.appWillForeground()
        XCTAssertFalse(state.isLocked,
            "Within 60 s grace period, appWillForeground must NOT set isLocked=true")
    }

    /// appDidBackground() must record the current date in lastForegroundedAt
    /// so the elapsed time calculation is correct on the next foreground.
    func test_appDidBackground_updatesLastForegroundedAt() {
        let state = AppState.shared
        let before = Date()
        state.appDidBackground()
        let after = Date()
        XCTAssertGreaterThanOrEqual(state.lastForegroundedAt, before,
            "lastForegroundedAt must be updated to ≥ the call time")
        XCTAssertLessThanOrEqual(state.lastForegroundedAt, after,
            "lastForegroundedAt must be updated to ≤ the call time")
    }

    // =========================================================================
    // MARK: 2. T-2.10 — AppState must publish background/foreground transitions
    //
    // FAILS until impl agent adds lifecycleTransitions publisher to AppState.
    // =========================================================================

    /// After appDidBackground(), lifecycleTransitions must emit `true`.
    ///
    /// FAILS: stub publisher never emits → expectation times out.
    func test_appDidBackground_publishesBackgroundTransition() {
        let state = AppState.shared
        let exp = expectation(description: "lifecycleTransitions emits true on background")

        state.lifecycleTransitions
            .filter { $0 == true }
            .first()
            .sink { _ in exp.fulfill() }
            .store(in: &cancellables)

        state.appDidBackground()

        // Stub publisher never emits → this wait TIMES OUT → test FAILS.
        // Impl agent: add an AnyPublisher<Bool, Never> on AppState that sends
        // `true` from appDidBackground() and `false` from appWillForeground().
        wait(for: [exp], timeout: 0.5)
    }

    /// After appWillForeground(), lifecycleTransitions must emit `false`.
    ///
    /// FAILS: stub publisher never emits → expectation times out.
    func test_appWillForeground_publishesForegroundTransition() async {
        let state = AppState.shared
        // Prime with a recent timestamp so FaceID gate is skipped.
        state.appDidBackground()

        let exp = expectation(description: "lifecycleTransitions emits false on foreground")

        state.lifecycleTransitions
            .filter { $0 == false }
            .first()
            .sink { _ in exp.fulfill() }
            .store(in: &cancellables)

        await state.appWillForeground()

        // Stub publisher never emits → this wait TIMES OUT → test FAILS.
        await fulfillment(of: [exp], timeout: 0.5)
    }

    /// lifecycleTransitions must emit in the correct order: true then false.
    ///
    /// FAILS: stub publisher never emits → first expectation times out.
    func test_lifecycleTransitions_correctOrder_backgroundThenForeground() async {
        let state = AppState.shared

        var emissions: [Bool] = []
        let backgroundExp = expectation(description: "background emission (true)")
        let foregroundExp = expectation(description: "foreground emission (false)")

        state.lifecycleTransitions
            .prefix(2)
            .collect()
            .sink { values in
                emissions = values
                if values.first == true { backgroundExp.fulfill() }
                if values.last == false { foregroundExp.fulfill() }
            }
            .store(in: &cancellables)

        state.appDidBackground()
        await state.appWillForeground()

        // Stub publisher never emits → both expectations time out → FAILS.
        await fulfillment(of: [backgroundExp, foregroundExp], timeout: 0.5)
        XCTAssertEqual(emissions, [true, false],
            "T-2.10: lifecycleTransitions must emit [true, false] for " +
            "background→foreground sequence")
    }

    // =========================================================================
    // MARK: 3. T-2.10 — Lifecycle transitions must be accessible for connection
    //         subscription without requiring access to ContentView internals.
    // =========================================================================

    /// lifecycleTransitions must be a valid publisher (non-nil) on AppState.
    /// Tests the structural presence of the publisher surface.
    ///
    /// PASSES immediately once the property is added; the emission tests above
    /// verify that it actually fires.
    func test_lifecycleTransitions_publisherExists() {
        // This test passes once lifecycleTransitions property exists on AppState.
        // It always FAILS because the stub (Empty publisher) technically exists,
        // but we add a stronger check: we verify the publisher has sent at least
        // one value by checking a side-channel flag.
        //
        // Use a simple check: the value we receive after posting a background event
        // is `true`. Since stub never emits, `receivedValue` stays nil → FAILS.

        let state = AppState.shared
        var receivedValue: Bool? = nil
        state.lifecycleTransitions
            .first()
            .sink { value in receivedValue = value }
            .store(in: &cancellables)

        state.appDidBackground()

        // Stub never emits → receivedValue stays nil → FAILS.
        XCTAssertNotNil(receivedValue,
            "T-2.10: AppState.lifecycleTransitions must emit when appDidBackground() is called. " +
            "Impl agent: add a PassthroughSubject<Bool, Never> to AppState, send(true) in " +
            "appDidBackground(), send(false) in appWillForeground(). Expose as " +
            "AnyPublisher<Bool, Never>.")
    }
}

// =============================================================================
// MARK: - Stub extension
//
// Provides `lifecycleTransitions` so the test file compiles before the impl.
// The Empty publisher never emits → all tests that wait for emissions FAIL.
//
// Impl agent: add a real `lifecycleTransitions: AnyPublisher<Bool, Never>` to
// AppState.swift backed by a PassthroughSubject. REMOVE this stub once added.
// =============================================================================

extension AppState {

    /// T-2.10: Emits `true` when the app enters background,
    ///         `false` when the app returns to foreground.
    ///
    /// STUB → Empty publisher, never emits → all T-2.10 emission tests FAIL.
    var lifecycleTransitions: AnyPublisher<Bool, Never> {
        Empty<Bool, Never>().eraseToAnyPublisher()
    }
}
