package wang.yaojia.webterm.testsupport

import kotlinx.coroutines.test.TestScope
import kotlin.time.ComparableTimeMark
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.TestTimeSource
import kotlin.time.TimeSource

/**
 * Manually-advanced [TimeSource] (A3, the Android analogue of iOS `FakeClock`). Drives reducers
 * that read elapsed time — `PingScheduler`, `AwayDigest`'s fade, telemetry staleness — with ZERO
 * real-time waiting: code under test reads a mark and the test moves time with [advance].
 *
 * Two readable shapes so it fits both reducer styles without inventing A5/A6's clock contract:
 *  - as a [TimeSource]: inject it and take [TimeSource.markNow]/[ComparableTimeMark.elapsedNow] for
 *    duration-based logic (ping interval, digest fade);
 *  - [epochMillis]: an epoch-ms reader for the `at`-timestamp staleness reducers
 *    (`StatusTelemetry.at` vs `Tunables.TELEMETRY_STALE_TTL_MS`).
 *
 * Thin wrapper over stdlib [TestTimeSource] (which owns the mark machinery); this class only tracks
 * the elapsed offset in parallel so [epochMillis]/[now] can be read. Not tied to the coroutine
 * scheduler — see [tick] to advance virtual `delay()` and this clock together in one call.
 */
@OptIn(ExperimentalTime::class)
public class FakeTimeSource(
    /** Wall-clock epoch (ms) reported at zero elapsed; [epochMillis] = this + elapsed. */
    public val epochBaseMillis: Long = DEFAULT_EPOCH_BASE_MILLIS,
) : TimeSource.WithComparableMarks {
    private val backing = TestTimeSource()
    private var elapsed: Duration = Duration.ZERO

    override fun markNow(): ComparableTimeMark = backing.markNow()

    /** Total time advanced since construction. */
    public val now: Duration
        get() = elapsed

    /** Current wall-clock reading in epoch milliseconds ([epochBaseMillis] + [now]). */
    public fun epochMillis(): Long = epochBaseMillis + elapsed.inWholeMilliseconds

    /** Move time forward (never backward); wakes any mark's [ComparableTimeMark.elapsedNow]. */
    public fun advance(by: Duration) {
        require(by >= Duration.ZERO) { "FakeTimeSource cannot move backwards (got $by)" }
        backing += by
        elapsed += by
    }

    /** `clock += 25.seconds` sugar over [advance]. */
    public operator fun plusAssign(by: Duration) {
        advance(by)
    }

    public companion object {
        public const val DEFAULT_EPOCH_BASE_MILLIS: Long = 0L
    }
}

/**
 * Advance BOTH the coroutine virtual clock and a [FakeTimeSource] by [duration] in one call, keeping
 * `delay()`-driven scheduling and elapsed-time reads in lockstep. Use inside `runTest` when a
 * reducer both suspends on `delay()` AND reads the injected clock:
 *
 *     runTest {
 *         val clock = FakeTimeSource()
 *         // ... start the reducer ...
 *         tick(clock, Tunables.PING_INTERVAL)   // fires the delay AND moves the readable clock
 *     }
 */
public fun TestScope.tick(clock: FakeTimeSource, duration: Duration) {
    testScheduler.advanceTimeBy(duration)
    testScheduler.runCurrent()
    clock.advance(duration)
}
