import Foundation
import Testing
import WireProtocol
@testable import SessionCore

/// T-iOS-6 · AwayDigest — "what happened while I was away" reducer (plan §3.2).
/// Input is the `GET /live-sessions/:id/events` timeline (src/types.ts:428-433);
/// class vocabulary is the server's `TimelineClass` union (src/types.ts:423,
/// produced by src/session/timeline.ts CLASS_MAP): tool/waiting/done/stuck/user.
@Suite("AwayDigest")
struct AwayDigestTests {
    // MARK: - helpers

    /// Chronology anchor: all test events sit at `leftAt + offset` ms.
    private static let leftAt = Date(timeIntervalSince1970: 1_000)
    private static let leftAtMs = 1_000_000
    private static let defaultLimit = 10

    private static func event(
        atOffsetMs offset: Int,
        class className: String,
        toolName: String? = nil,
        label: String = "activity"
    ) -> TimelineEvent {
        TimelineEvent(at: leftAtMs + offset, class: className, toolName: toolName, label: label)
    }

    // MARK: - counting

    @Test("counts tool runs, waiting events, and done across the event list")
    func countsToolWaitingAndDoneAcrossEvents() {
        // Arrange: 3 tool + 1 waiting + 1 done (task spec's canonical example).
        let events = [
            Self.event(atOffsetMs: 1, class: "tool", toolName: "Bash", label: "ran Bash"),
            Self.event(atOffsetMs: 2, class: "tool", toolName: "Edit", label: "edited with Edit"),
            Self.event(atOffsetMs: 3, class: "waiting", label: "waiting for approval"),
            Self.event(atOffsetMs: 4, class: "tool", toolName: "Write", label: "edited with Write"),
            Self.event(atOffsetMs: 5, class: "done", label: "done"),
        ]

        // Act
        let digest = AwayDigest.reduce(events: events, since: Self.leftAt, limit: Self.defaultLimit)

        // Assert
        #expect(digest.toolRuns == 3)
        #expect(digest.waitingCount == 1)
        #expect(digest.sawDone == true)
        #expect(digest.sawStuck == false)
    }

    @Test("a stuck event sets sawStuck")
    func stuckEventSetsSawStuck() {
        // Arrange
        let events = [Self.event(atOffsetMs: 1, class: "stuck", label: "stuck")]

        // Act
        let digest = AwayDigest.reduce(events: events, since: Self.leftAt, limit: Self.defaultLimit)

        // Assert
        #expect(digest.sawStuck == true)
        #expect(digest.sawDone == false)
    }

    @Test("user events appear in recent but count toward no metric")
    func userEventsAppearInRecentButCountNothing() {
        // Arrange
        let userEvent = Self.event(atOffsetMs: 1, class: "user", label: "user input")

        // Act
        let digest = AwayDigest.reduce(events: [userEvent], since: Self.leftAt, limit: Self.defaultLimit)

        // Assert
        #expect(digest.toolRuns == 0)
        #expect(digest.waitingCount == 0)
        #expect(digest.sawDone == false)
        #expect(digest.sawStuck == false)
        #expect(digest.recent == [userEvent])
    }

    // MARK: - since filter

    @Test("events strictly earlier than since are excluded from counts and recent")
    func eventsBeforeSinceAreExcluded() {
        // Arrange: one stale tool run before leaving, one fresh after.
        let stale = Self.event(atOffsetMs: -1, class: "tool", label: "ran Bash")
        let boundary = Self.event(atOffsetMs: 0, class: "waiting", label: "waiting for approval")
        let fresh = Self.event(atOffsetMs: 1, class: "tool", label: "ran Edit")

        // Act
        let digest = AwayDigest.reduce(
            events: [stale, boundary, fresh], since: Self.leftAt, limit: Self.defaultLimit)

        // Assert: strictly-earlier excluded; the exact leave instant still counts.
        #expect(digest.toolRuns == 1)
        #expect(digest.waitingCount == 1)
        #expect(digest.recent == [boundary, fresh])
    }

    @Test("all events before since yield the all-zero digest")
    func allEventsBeforeSinceYieldAllZeroDigest() {
        // Arrange
        let events = [
            Self.event(atOffsetMs: -2, class: "tool", label: "ran Bash"),
            Self.event(atOffsetMs: -1, class: "done", label: "done"),
        ]

        // Act
        let digest = AwayDigest.reduce(events: events, since: Self.leftAt, limit: Self.defaultLimit)

        // Assert: UI can skip rendering entirely.
        #expect(digest == .empty)
        #expect(digest.isEmpty == true)
    }

    // MARK: - limit / recent

    @Test("limit truncates recent to the most recent events in chronological order")
    func limitTruncatesRecentToMostRecentEvents() {
        // Arrange
        let events = (1...5).map { Self.event(atOffsetMs: $0, class: "tool", label: "ran \($0)") }

        // Act
        let digest = AwayDigest.reduce(events: events, since: Self.leftAt, limit: 2)

        // Assert: newest two, still oldest→newest.
        #expect(digest.recent == [events[3], events[4]])
        #expect(digest.toolRuns == 5)
    }

    @Test("out-of-order server input is ordered by timestamp before truncating recent")
    func outOfOrderEventsAreOrderedByTimestampForRecent() {
        // Arrange: the server is an untrusted input source — order is not a given.
        let oldest = Self.event(atOffsetMs: 1, class: "tool", label: "ran 1")
        let middle = Self.event(atOffsetMs: 2, class: "waiting", label: "waiting")
        let newest = Self.event(atOffsetMs: 3, class: "done", label: "done")

        // Act
        let digest = AwayDigest.reduce(
            events: [newest, oldest, middle], since: Self.leftAt, limit: 2)

        // Assert
        #expect(digest.recent == [middle, newest])
    }

    @Test("zero or negative limit yields empty recent while counts still compute")
    func zeroOrNegativeLimitYieldsEmptyRecentButCountsStillComputed() {
        // Arrange
        let events = [Self.event(atOffsetMs: 1, class: "tool", label: "ran Bash")]

        // Act
        let zero = AwayDigest.reduce(events: events, since: Self.leftAt, limit: 0)
        let negative = AwayDigest.reduce(events: events, since: Self.leftAt, limit: -3)

        // Assert
        #expect(zero.recent.isEmpty)
        #expect(zero.toolRuns == 1)
        #expect(negative.recent.isEmpty)
        #expect(negative.toolRuns == 1)
    }

    // MARK: - empty / extreme inputs

    @Test("empty events yield the all-zero digest")
    func emptyEventsYieldAllZeroDigest() {
        // Arrange / Act
        let digest = AwayDigest.reduce(events: [], since: Self.leftAt, limit: Self.defaultLimit)

        // Assert
        #expect(digest == AwayDigest(
            toolRuns: 0, waitingCount: 0, sawDone: false, sawStuck: false, recent: []))
        #expect(digest.isEmpty == true)
    }

    @Test("extreme since dates never crash the ms conversion")
    func extremeSinceDatesDoNotCrash() {
        // Arrange
        let events = [Self.event(atOffsetMs: 1, class: "tool", label: "ran Bash")]

        // Act
        let future = AwayDigest.reduce(events: events, since: .distantFuture, limit: 5)
        let past = AwayDigest.reduce(events: events, since: .distantPast, limit: 5)
        let overflow = AwayDigest.reduce(
            events: events, since: Date(timeIntervalSince1970: .greatestFiniteMagnitude), limit: 5)

        // Assert
        #expect(future.isEmpty == true)
        #expect(past.toolRuns == 1)
        #expect(overflow.isEmpty == true)
    }

    // MARK: - vocabulary sync

    @Test("digest class vocabulary stays in sync with WireProtocol's known classes")
    func classVocabularyStaysInSyncWithWireProtocol() {
        // Arrange: the local names must be exactly the server's TimelineClass
        // union as frozen in WireProtocol (src/types.ts:423).
        let local: Set<String> = [
            TimelineClassName.tool, TimelineClassName.waiting, TimelineClassName.done,
            TimelineClassName.stuck, TimelineClassName.user,
        ]

        // Act / Assert
        #expect(local == TimelineEvent.knownClasses)
    }
}
