"""Tests for the pytest plugin module."""

from __future__ import annotations

import pytest

from codeflash_python.testing._pytest_plugin import (
    SECONDS_IN_HOUR,
    SECONDS_IN_MINUTE,
    SHORTEST_AMOUNT_OF_TIME,
    STABILITY_CENTER_TOLERANCE,
    STABILITY_SPREAD_TOLERANCE,
    STABILITY_WINDOW_SIZE,
    InvalidTimeParameterError,
    UnexpectedError,
    get_runtime_from_stdout,
    should_stop,
)


class TestGetRuntimeFromStdout:
    """get_runtime_from_stdout marker-based runtime extraction."""

    def test_valid_markers_with_runtime(self) -> None:
        """Stdout containing markers and an integer runtime returns the value."""
        stdout = "some output\n!######test_name:12345######!\nmore output"

        assert 12345 == get_runtime_from_stdout(stdout)

    def test_colon_separated_payload(self) -> None:
        """Payload with colon-separated prefix extracts the integer after the last colon."""
        stdout = "!######module.test_func:99999######!"

        assert 99999 == get_runtime_from_stdout(stdout)

    def test_multiple_marker_pairs_returns_last(self) -> None:
        """When multiple marker pairs exist, the last runtime is returned."""
        stdout = "!######first:100######!\n!######second:200######!\n"
        result = get_runtime_from_stdout(stdout)

        assert 200 == result

    def test_empty_string_returns_none(self) -> None:
        """Empty stdout returns None."""
        assert get_runtime_from_stdout("") is None

    def test_no_markers_returns_none(self) -> None:
        """Stdout without markers returns None."""
        assert get_runtime_from_stdout("just some output") is None

    def test_missing_end_marker_returns_none(self) -> None:
        """Stdout with only the start marker returns None."""
        assert get_runtime_from_stdout("!######value") is None

    def test_missing_start_marker_returns_none(self) -> None:
        """Stdout with only the end marker returns None."""
        assert get_runtime_from_stdout("value######!") is None

    def test_non_integer_value_returns_none(self) -> None:
        """Non-integer payload between markers returns None."""
        assert get_runtime_from_stdout("!######not_a_number######!") is None

    def test_payload_with_only_colon_no_int_returns_none(self) -> None:
        """Payload ending with colon but no integer returns None."""
        assert get_runtime_from_stdout("!######key:abc######!") is None

    def test_simple_integer_payload_no_colon(self) -> None:
        """Payload without a colon returns None (colon required)."""
        stdout = "!######42######!"

        assert get_runtime_from_stdout(stdout) is None

    def test_markers_with_surrounding_whitespace(self) -> None:
        """Markers surrounded by whitespace still parse correctly."""
        stdout = "   !######test:500######!   "

        assert 500 == get_runtime_from_stdout(stdout)


class TestShouldStop:
    """should_stop stability-based early stopping logic."""

    def test_returns_false_when_fewer_than_window(self) -> None:
        """Returns False when runtimes has fewer entries than window."""
        assert should_stop([100, 200], window=5, min_window_size=1) is False

    def test_returns_false_when_fewer_than_min_window_size(self) -> None:
        """Returns False when runtimes has fewer entries than min_window_size."""
        assert should_stop([100, 200], window=2, min_window_size=5) is False

    def test_returns_true_when_all_identical(self) -> None:
        """Identical values are perfectly stable."""
        runtimes = [1000] * 10
        assert should_stop(runtimes, window=5, min_window_size=3) is True

    def test_returns_false_when_high_variance(self) -> None:
        """Wildly varying runtimes are not stable."""
        runtimes = [100, 10000, 100, 10000, 100, 10000, 100, 10000]
        assert should_stop(runtimes, window=4, min_window_size=3) is False

    def test_returns_false_when_min_is_zero(self) -> None:
        """Returns False when the minimum runtime value is 0."""
        runtimes = [0, 0, 0, 0, 0]
        assert should_stop(runtimes, window=3, min_window_size=2) is False

    def test_window_size_of_one_with_stable_tail(self) -> None:
        """Window of 1 always considers the last value stable by itself."""
        runtimes = [1000, 2000, 3000]
        result = should_stop(runtimes, window=1, min_window_size=1)

        assert result is True

    def test_odd_window_size_median(self) -> None:
        """Odd window uses the exact middle element as median."""
        runtimes = [1000, 1001, 1000, 1001, 1000]
        assert (
            should_stop(
                runtimes,
                window=5,
                min_window_size=3,
                center_rel_tol=0.01,
                spread_rel_tol=0.01,
            )
            is True
        )

    def test_even_window_size_median(self) -> None:
        """Even window computes median correctly."""
        runtimes = [1000, 1001, 1000, 1001]
        assert (
            should_stop(
                runtimes,
                window=4,
                min_window_size=3,
                center_rel_tol=0.01,
                spread_rel_tol=0.01,
            )
            is True
        )

    def test_custom_tolerance_strict(self) -> None:
        """Very tight tolerance rejects slight variation."""
        runtimes = [1000, 1010, 1000, 1010, 1000]
        assert (
            should_stop(
                runtimes,
                window=5,
                min_window_size=3,
                center_rel_tol=0.0001,
                spread_rel_tol=0.0001,
            )
            is False
        )

    def test_custom_tolerance_relaxed(self) -> None:
        """Relaxed tolerance accepts moderate variation."""
        runtimes = [1000, 1050, 1000, 1050, 1000]
        assert (
            should_stop(
                runtimes,
                window=5,
                min_window_size=3,
                center_rel_tol=0.1,
                spread_rel_tol=0.1,
            )
            is True
        )

    def test_uses_last_window_entries(self) -> None:
        """Only the last `window` entries matter, not earlier noisy data."""
        runtimes = [1, 99999, 50000, 1000, 1000, 1000, 1000, 1000]
        assert should_stop(runtimes, window=5, min_window_size=3) is True

    def test_empty_runtimes_returns_false(self) -> None:
        """Empty list of runtimes returns False."""
        assert should_stop([], window=5, min_window_size=1) is False


class TestConstants:
    """Verify module-level constant values."""

    def test_seconds_in_hour(self) -> None:
        """SECONDS_IN_HOUR equals 3600.0."""
        assert 3600.0 == SECONDS_IN_HOUR

    def test_seconds_in_minute(self) -> None:
        """SECONDS_IN_MINUTE equals 60.0."""
        assert 60.0 == SECONDS_IN_MINUTE

    def test_shortest_amount_of_time(self) -> None:
        """SHORTEST_AMOUNT_OF_TIME equals 0.0."""
        assert 0.0 == SHORTEST_AMOUNT_OF_TIME

    def test_stability_window_size(self) -> None:
        """STABILITY_WINDOW_SIZE equals 0.35."""
        assert 0.35 == STABILITY_WINDOW_SIZE

    def test_stability_center_tolerance(self) -> None:
        """STABILITY_CENTER_TOLERANCE equals 0.0025."""
        assert 0.0025 == STABILITY_CENTER_TOLERANCE

    def test_stability_spread_tolerance(self) -> None:
        """STABILITY_SPREAD_TOLERANCE equals 0.0025."""
        assert 0.0025 == STABILITY_SPREAD_TOLERANCE


class TestExceptions:
    """Verify custom exception classes."""

    def test_invalid_time_parameter_error_is_exception(self) -> None:
        """InvalidTimeParameterError is a subclass of Exception."""
        assert issubclass(InvalidTimeParameterError, Exception)

    def test_unexpected_error_is_exception(self) -> None:
        """UnexpectedError is a subclass of Exception."""
        assert issubclass(UnexpectedError, Exception)

    def test_invalid_time_parameter_error_can_be_raised(self) -> None:
        """InvalidTimeParameterError can be raised and caught."""
        with pytest.raises(InvalidTimeParameterError):
            raise InvalidTimeParameterError

    def test_unexpected_error_can_be_raised(self) -> None:
        """UnexpectedError can be raised and caught."""
        with pytest.raises(UnexpectedError):
            raise UnexpectedError

    def test_invalid_time_parameter_error_message(self) -> None:
        """InvalidTimeParameterError preserves its message."""
        err = InvalidTimeParameterError("total time < 0")

        assert "total time < 0" in str(err)

    def test_unexpected_error_message(self) -> None:
        """UnexpectedError preserves its message."""
        err = UnexpectedError("unsupported")

        assert "unsupported" in str(err)
