"""Tests for autodebug_replay/time_replay.py deterministic replay patches.

Verifies that time, datetime, random, uuid, and os.urandom are correctly
patched to return recorded transcript values, and that originals are
restored on uninstall.
"""

from __future__ import annotations

import datetime
import os
import random
import time
import uuid

import pytest

from autodebug_replay import time_replay
from autodebug_replay.transcripts import TranscriptIndex


@pytest.fixture(autouse=True)
def _cleanup_replay():
    """Ensure time_replay is always uninstalled after every test."""
    yield
    time_replay.uninstall()


# ---- helpers ---------------------------------------------------------------

def _make_index(records):
    """Build a TranscriptIndex from a list of record dicts."""
    return TranscriptIndex(records)


# ---- TIME tests ------------------------------------------------------------

class TestTimeReturnsRecordedValues:
    """time.time() returns values from TIME records in sequence order."""

    def test_time_returns_recorded_values(self):
        index = _make_index([
            {"type": "TIME", "seq": 1, "value": 1000.5},
            {"type": "TIME", "seq": 2, "value": 2000.75},
            {"type": "TIME", "seq": 3, "value": 3000.0},
        ])
        time_replay.install(index)

        assert time.time() == 1000.5
        assert time.time() == 2000.75
        assert time.time() == 3000.0


class TestTimeFallbackIncrements:
    """After TIME records are exhausted, fallback values increment."""

    def test_time_fallback_increments(self):
        index = _make_index([
            {"type": "TIME", "seq": 1, "value": 100.0},
        ])
        time_replay.install(index)

        # Consume the single record
        assert time.time() == 100.0

        # Fallback values should increment from _FALLBACK_TIME
        fb1 = time.time()
        fb2 = time.time()
        assert fb2 > fb1, "Fallback values must strictly increase"
        assert fb1 == 1700000001.0  # _FALLBACK_TIME + 1
        assert fb2 == 1700000002.0  # _FALLBACK_TIME + 2


class TestFallbackCounterResetsOnInstall:
    """Calling install() a second time resets the fallback counter."""

    def test_fallback_counter_resets_on_install(self):
        # First install -- exhaust records, push fallback counter
        index1 = _make_index([])
        time_replay.install(index1)
        first_fb = time.time()  # counter = 1
        _ = time.time()         # counter = 2

        # Second install -- counter must reset
        index2 = _make_index([])
        time_replay.install(index2)
        reset_fb = time.time()  # counter should be 1 again

        assert reset_fb == first_fb, "Fallback counter must reset on re-install"


# ---- RNG tests -------------------------------------------------------------

class TestRngRandomReturnsFloat:
    """random.random() returns a float from an RNG record."""

    def test_rng_random_returns_float(self):
        index = _make_index([
            {"type": "RNG", "seq": 1, "value": 0.42},
        ])
        time_replay.install(index)

        result = random.random()
        assert result == 0.42
        assert isinstance(result, float)


class TestRngRandintReturnsInt:
    """random.randint() returns an int (not float) from an RNG record."""

    def test_rng_randint_returns_int(self):
        index = _make_index([
            {"type": "RNG", "seq": 1, "value": 7},
        ])
        time_replay.install(index)

        result = random.randint(1, 10)
        assert result == 7
        assert isinstance(result, int), "randint must return int, not float"


class TestRngChoiceReturnsElement:
    """random.choice() uses the index value to select from the provided list."""

    def test_rng_choice_returns_element(self):
        choices = ["alpha", "beta", "gamma", "delta"]
        index = _make_index([
            {"type": "RNG", "seq": 1, "value": 2},  # index 2 -> "gamma"
        ])
        time_replay.install(index)

        result = random.choice(choices)
        assert result == "gamma"


class TestRngShuffleMutatesList:
    """random.shuffle() mutates the list in place from a recorded value."""

    def test_rng_shuffle_mutates_list(self):
        original = [1, 2, 3, 4]
        recorded_order = [4, 2, 1, 3]
        index = _make_index([
            {"type": "RNG", "seq": 1, "value": recorded_order},
        ])
        time_replay.install(index)

        data = list(original)
        result = random.shuffle(data)

        assert result is None, "shuffle must return None"
        assert data == recorded_order


# ---- UUID tests ------------------------------------------------------------

class TestUuid4ReturnsRecordedUuid:
    """uuid.uuid4() returns the UUID from a UUID record."""

    def test_uuid4_returns_recorded_uuid(self):
        expected = "12345678-1234-4234-8234-123456789abc"
        index = _make_index([
            {"type": "UUID", "seq": 1, "value": expected},
        ])
        time_replay.install(index)

        result = uuid.uuid4()
        assert isinstance(result, uuid.UUID)
        assert str(result) == expected


class TestUuid4FallbackIsDeterministic:
    """With no UUID records, uuid4() fallback is deterministic."""

    def test_uuid4_fallback_is_deterministic(self):
        index = _make_index([])
        time_replay.install(index)

        first = uuid.uuid4()
        second = uuid.uuid4()

        assert isinstance(first, uuid.UUID)
        assert isinstance(second, uuid.UUID)
        assert first != second, "Successive fallback UUIDs must differ"

        # Re-install to verify deterministic replay of fallback sequence
        index2 = _make_index([])
        time_replay.install(index2)

        replay_first = uuid.uuid4()
        replay_second = uuid.uuid4()

        assert replay_first == first, "Fallback UUID sequence must be deterministic"
        assert replay_second == second


# ---- datetime tests --------------------------------------------------------

class TestDatetimeNowReturnsDatetime:
    """datetime.datetime.now() returns a datetime built from a TIME record."""

    def test_datetime_now_returns_datetime(self):
        ts = 1700000000.0  # 2023-11-14 22:13:20 UTC
        index = _make_index([
            {"type": "TIME", "seq": 1, "value": ts},
        ])
        time_replay.install(index)

        result = datetime.datetime.now()
        assert isinstance(result, datetime.datetime)
        expected = datetime.datetime.fromtimestamp(ts)
        assert result == expected


class TestDatetimeUtcnowReturnsDatetime:
    """datetime.datetime.utcnow() returns the correct UTC datetime."""

    def test_datetime_utcnow_returns_datetime(self):
        ts = 1700000000.0
        index = _make_index([
            {"type": "TIME", "seq": 1, "value": ts},
        ])
        time_replay.install(index)

        result = datetime.datetime.utcnow()
        assert isinstance(result, datetime.datetime)
        expected = datetime.datetime.utcfromtimestamp(ts)
        assert result == expected


# ---- os.urandom tests ------------------------------------------------------

class TestUrandomReturnsBytes:
    """os.urandom() returns bytes decoded from the hex value in an RNG record."""

    def test_urandom_returns_bytes(self):
        hex_value = "deadbeef"
        index = _make_index([
            {"type": "RNG", "seq": 1, "value": hex_value},
        ])
        time_replay.install(index)

        result = os.urandom(4)
        assert isinstance(result, bytes)
        assert result == bytes.fromhex(hex_value)[:4]


# ---- uninstall tests -------------------------------------------------------

class TestUninstallRestoresOriginals:
    """After uninstall(), all patched functions return to their originals."""

    def test_uninstall_restores_originals(self):
        # Capture references to the real functions before install
        orig_time = time.time
        orig_monotonic = time.monotonic
        orig_random = random.random
        orig_randint = random.randint
        orig_choice = random.choice
        orig_uuid4 = uuid.uuid4
        orig_urandom = os.urandom

        index = _make_index([
            {"type": "TIME", "seq": 1, "value": 1.0},
        ])
        time_replay.install(index)

        # Verify they are patched (different from originals)
        assert time.time is not orig_time
        assert random.random is not orig_random

        time_replay.uninstall()

        # Verify originals are restored
        assert time.time is orig_time
        assert time.monotonic is orig_monotonic
        assert random.random is orig_random
        assert random.randint is orig_randint
        assert random.choice is orig_choice
        assert uuid.uuid4 is orig_uuid4
        assert os.urandom is orig_urandom
