import Foundation
import Testing
import WireProtocol
@testable import SessionCore

/// T-iOS-23 · UnreadLedger — pure unread-watermark bookkeeping (plan §7).
/// Mirrors the web tab dot semantics (public/tabs.ts `hasActivity`, viewing
/// clears it) against the server's `lastOutputAt` snapshot (T-iOS-37 field):
/// a session is unread iff its `lastOutputAt` is strictly newer than the local
/// last-seen watermark. Persistence-agnostic — the App layer wires UserDefaults.
@Suite("UnreadLedger")
struct UnreadLedgerTests {
    private let sessionId = UUID()
    private let seenAt = 1_700_000_100_000

    // MARK: - Unread判定（lastOutputAt vs 水位）

    @Test("lastOutputAt 严格大于已记录水位 → unread；等于/小于 → 已读")
    func unreadIsStrictlyNewerThanWatermark() {
        // Arrange
        let ledger = UnreadLedger().record(seen: sessionId, at: seenAt)

        // Assert
        #expect(ledger.isUnread(sessionId: sessionId, lastOutputAt: seenAt + 1))
        #expect(!ledger.isUnread(sessionId: sessionId, lastOutputAt: seenAt))
        #expect(!ledger.isUnread(sessionId: sessionId, lastOutputAt: seenAt - 1))
    }

    @Test("从未看过的会话（无水位）：任何正的 lastOutputAt 都是 unread")
    func neverSeenSessionIsUnread() {
        // Arrange
        let ledger = UnreadLedger()

        // Assert: 水位缺省按 0 —— 服务器保证 lastOutputAt ≥ createdAt > 0。
        #expect(ledger.isUnread(sessionId: sessionId, lastOutputAt: 1))
        #expect(ledger.isUnread(sessionId: sessionId, lastOutputAt: seenAt))
    }

    @Test("lastOutputAt 缺失（旧服务器）或非正值（不可信输入）→ 绝不显示 unread")
    func missingOrGarbageLastOutputAtIsNeverUnread() {
        // Arrange
        let ledger = UnreadLedger()

        // Assert: nil = 服务器没给数据；0/负值 = 畸形数据 —— 都不点灯。
        #expect(!ledger.isUnread(sessionId: sessionId, lastOutputAt: nil))
        #expect(!ledger.isUnread(sessionId: sessionId, lastOutputAt: 0))
        #expect(!ledger.isUnread(sessionId: sessionId, lastOutputAt: -5))
    }

    // MARK: - record：不可变 + 单调

    @Test("record 返回新副本，原 ledger 不变（不可变风格）")
    func recordIsImmutable() {
        // Arrange
        let original = UnreadLedger()

        // Act
        let recorded = original.record(seen: sessionId, at: seenAt)

        // Assert
        #expect(original == UnreadLedger())
        #expect(original.watermarks[sessionId] == nil)
        #expect(recorded.watermarks[sessionId] == seenAt)
    }

    @Test("record 单调不回退：更早的时间不会降低已有水位")
    func recordIsMonotonic() {
        // Arrange
        let ledger = UnreadLedger().record(seen: sessionId, at: seenAt)

        // Act: 乱序到达的更早 record（如迟到的回调）。
        let after = ledger.record(seen: sessionId, at: seenAt - 1_000)

        // Assert
        #expect(after.watermarks[sessionId] == seenAt)
        #expect(!after.isUnread(sessionId: sessionId, lastOutputAt: seenAt))
    }

    // MARK: - 容量上限（持久化字典不可无界增长）

    @Test("超过 maxEntries 时丢弃最旧水位，保留最新的 maxEntries 条")
    func capDropsOldestWatermarks() throws {
        // Arrange: 恰好灌满上限。
        var ledger = UnreadLedger()
        for offset in 0..<UnreadLedger.maxEntries {
            ledger = ledger.record(seen: UUID(), at: seenAt + offset)
        }
        let oldestId = try #require(ledger.watermarks.min { $0.value < $1.value }?.key)

        // Act: 再多一条 —— 最旧的那条必须让位。
        let newcomer = UUID()
        let capped = ledger.record(seen: newcomer, at: seenAt + UnreadLedger.maxEntries)

        // Assert
        #expect(capped.watermarks.count == UnreadLedger.maxEntries)
        #expect(capped.watermarks[newcomer] != nil)
        #expect(capped.watermarks[oldestId] == nil)
    }

    @Test("init 从持久化载入时同样施加上限（旧存量超限不带病复活）")
    func initCapsOversizedPersistedState() {
        // Arrange: 直接构造一个超限字典（模拟旧版本存量）。
        let oversized = Dictionary(
            uniqueKeysWithValues: (0..<(UnreadLedger.maxEntries + 10)).map {
                (UUID(), seenAt + $0)
            }
        )

        // Act
        let ledger = UnreadLedger(watermarks: oversized)

        // Assert: 截到上限，且留下的是最新的（最小值被丢弃端）。
        #expect(ledger.watermarks.count == UnreadLedger.maxEntries)
        let keptMin = ledger.watermarks.values.min()
        #expect(keptMin == seenAt + 10)
    }
}
