import Foundation
import Testing
@testable import Workouts_Watch_App

/// Pins the pure session-end decision (`SessionEndPlanner.decide`) — the rule that ends the
/// watch's `HKWorkoutSession` from the authoritative workout set instead of a view observer.
/// The decisive properties: a running session ends only on a genuine non-empty → empty
/// transition of the active set; a survivor that's `.completed` is a finish, otherwise a
/// discard; and the launch race (session running before the run doc has synced) resolves to
/// `.none` so a real run is never discarded before we've heard about it.
struct SessionEndPlannerTests {
    private static let ts = Date(timeIntervalSince1970: 1_700_000_000)

    private func workout(_ id: String, _ status: WorkoutStatus) -> WorkoutDocument {
        WorkoutDocument(
            schemaVersion: WorkoutDocument.currentSchemaVersion,
            id: id, splitID: nil, splitName: nil, start: Self.ts, end: nil,
            status: status.rawValue, createdAt: Self.ts, updatedAt: Self.ts,
            logs: [], metrics: nil)
    }

    // MARK: - decide

    @Test func launchRace_runningButNothingHeardYet_isNone() {
        // Session running before any run document has synced: previous active set empty →
        // never discard a run we simply don't know about yet.
        let d = SessionEndPlanner.decide(isRunning: true, previouslyActiveIDs: [], workouts: [])
        #expect(d == .none)
    }

    @Test func completedSurvivor_isFinish() {
        let done = workout("A", .completed)
        let d = SessionEndPlanner.decide(
            isRunning: true, previouslyActiveIDs: ["A"], workouts: [done])
        #expect(d == .finish(done))
    }

    @Test func vanishedRun_isDiscard() {
        // "A" was active, now absent entirely (discarded/deleted on the phone) → discard.
        let d = SessionEndPlanner.decide(
            isRunning: true, previouslyActiveIDs: ["A"], workouts: [])
        #expect(d == .discard)
    }

    @Test func stillPartlyActive_isNone() {
        // One of two previously-active runs finished, the other is still in progress → the
        // active set isn't empty, so the session keeps running.
        let d = SessionEndPlanner.decide(
            isRunning: true,
            previouslyActiveIDs: ["A", "B"],
            workouts: [workout("A", .completed), workout("B", .inProgress)])
        #expect(d == .none)
    }

    @Test func notRunning_isNone() {
        // A cold-relaunched watch with no session must never try to end one.
        let d = SessionEndPlanner.decide(
            isRunning: false, previouslyActiveIDs: ["A"], workouts: [workout("A", .completed)])
        #expect(d == .none)
    }

    @Test func alreadyEnded_isNone() {
        // Baseline already empty (we ended on a prior reconcile) — a redelivered completed
        // run must not re-trigger the end.
        let d = SessionEndPlanner.decide(
            isRunning: true, previouslyActiveIDs: [], workouts: [workout("A", .completed)])
        #expect(d == .none)
    }

    @Test func skippedOnlySurvivor_isDiscard() {
        // A run that resolved entirely to `.skipped` (ended early, nothing completed) is not a
        // finish — there's no completed run to save, so the session data is discarded.
        let d = SessionEndPlanner.decide(
            isRunning: true, previouslyActiveIDs: ["A"], workouts: [workout("A", .skipped)])
        #expect(d == .discard)
    }

    // MARK: - activeIDs

    @Test func activeIDs_countsOnlyUnfinished() {
        let workouts = [
            workout("A", .notStarted),
            workout("B", .inProgress),
            workout("C", .completed),
            workout("D", .skipped),
        ]
        #expect(SessionEndPlanner.activeIDs(in: workouts) == ["A", "B"])
    }
}
