"""Tests for the debug-window launch budget.

The budget prevents two failure modes: crash loops (window vanishes within
seconds of launch → relaunching just repeats the crash) and volume loops
(something reopening the window over and over). Both are deterministic rules
in this module, not heuristics delegated to the model.
"""

from __future__ import annotations

import json
import os
from pathlib import Path

import pytest

from a360_mcp.browser.launch_budget import (
    LAUNCH_WINDOW_S,
    MAX_LAUNCHES_PER_WINDOW,
    MIN_RELAUNCH_INTERVAL_S,
    LaunchBudgetExceeded,
    budget_status,
    check_launch_allowed,
    recent_launches,
    record_launch,
)

_NOW = 1_000_000.0


def _path(tmp_path: Path) -> str:
    return str(tmp_path / "history.json")


class TestCheckLaunchAllowed:
    def test_no_history_passes(self, tmp_path):
        check_launch_allowed(_path(tmp_path), now=_NOW)

    def test_old_history_passes(self, tmp_path):
        p = _path(tmp_path)
        # A launch far outside the window — irrelevant.
        record_launch(p, now=_NOW - LAUNCH_WINDOW_S - 1)
        check_launch_allowed(p, now=_NOW)

    def test_too_soon_relaunch_blocked(self, tmp_path):
        p = _path(tmp_path)
        record_launch(p, now=_NOW)
        with pytest.raises(LaunchBudgetExceeded, match="ago and is already gone"):
            check_launch_allowed(p, now=_NOW + MIN_RELAUNCH_INTERVAL_S - 1)

    def test_exactly_interval_passes(self, tmp_path):
        p = _path(tmp_path)
        record_launch(p, now=_NOW)
        check_launch_allowed(p, now=_NOW + MIN_RELAUNCH_INTERVAL_S)

    def test_over_capacity_blocked(self, tmp_path):
        p = _path(tmp_path)
        for i in range(MAX_LAUNCHES_PER_WINDOW):
            record_launch(p, now=_NOW - 100 + i)
        with pytest.raises(LaunchBudgetExceeded, match="loop"):
            check_launch_allowed(p, now=_NOW)

    def test_read_only_does_not_consume(self, tmp_path):
        """A blocked launch check should NOT record a new launch."""
        p = _path(tmp_path)
        record_launch(p, now=_NOW)
        with pytest.raises(LaunchBudgetExceeded):
            check_launch_allowed(p, now=_NOW + 1)
        # Still only 1 entry.
        stamps = recent_launches(p, now=_NOW + 1)
        assert len(stamps) == 1


class TestRecordLaunch:
    def test_records_timestamp(self, tmp_path):
        p = _path(tmp_path)
        record_launch(p, now=_NOW)
        stamps = recent_launches(p, now=_NOW)
        assert stamps == [_NOW]

    def test_appends_multiple(self, tmp_path):
        p = _path(tmp_path)
        for i in range(3):
            record_launch(p, now=_NOW + i)
        stamps = recent_launches(p, now=_NOW + 10)
        assert len(stamps) == 3

    def test_creates_parent_dir(self, tmp_path):
        p = str(tmp_path / "nested" / "dir" / "history.json")
        record_launch(p, now=_NOW)
        assert os.path.isfile(p)

    def test_capped_at_max_recorded(self, tmp_path):
        """The history file is trimmed to _MAX_RECORDED for disk hygiene."""
        from a360_mcp.browser.launch_budget import _MAX_RECORDED

        p = _path(tmp_path)
        for i in range(_MAX_RECORDED + 10):
            record_launch(p, now=_NOW + i)
        with open(p) as f:
            stored = json.load(f)
        assert len(stored) == _MAX_RECORDED


class TestRecentLaunches:
    def test_filters_by_window(self, tmp_path):
        p = _path(tmp_path)
        record_launch(p, now=_NOW - 1000)
        record_launch(p, now=_NOW - 10)
        record_launch(p, now=_NOW)
        active = recent_launches(p, now=_NOW, window_s=LAUNCH_WINDOW_S)
        assert len(active) == 2  # last two are within window

    def test_empty_when_no_file(self, tmp_path):
        assert recent_launches(_path(tmp_path), now=_NOW) == []

    def test_corrupt_file_returns_empty(self, tmp_path):
        p = _path(tmp_path)
        Path(p).write_text("not json{")
        assert recent_launches(p, now=_NOW) == []

    def test_non_list_file_returns_empty(self, tmp_path):
        p = _path(tmp_path)
        Path(p).write_text('{"key": "value"}')
        assert recent_launches(p, now=_NOW) == []

    def test_non_numeric_entries_filtered(self, tmp_path):
        # Use timestamps within the window so recent_launches doesn't drop them.
        from a360_mcp.browser.launch_budget import _read_history

        p = _path(tmp_path)
        Path(p).write_text('["a", 100, true, 200]')
        # _read_history is the raw parser; bool is int in Python → True=1.0.
        stamps = _read_history(p)
        assert stamps == [100.0, 1.0, 200.0]


class TestBudgetStatus:
    def test_empty_returns_zero_max(self, tmp_path):
        used, allowance = budget_status(_path(tmp_path), now=_NOW)
        assert used == 0
        assert allowance == MAX_LAUNCHES_PER_WINDOW

    def test_counts_active(self, tmp_path):
        p = _path(tmp_path)
        record_launch(p, now=_NOW - 10)
        record_launch(p, now=_NOW - 5)
        used, allowance = budget_status(p, now=_NOW)
        assert used == 2
        assert allowance == MAX_LAUNCHES_PER_WINDOW

    def test_excludes_old(self, tmp_path):
        p = _path(tmp_path)
        record_launch(p, now=_NOW - LAUNCH_WINDOW_S - 1)
        record_launch(p, now=_NOW)
        used, _ = budget_status(p, now=_NOW)
        assert used == 1


class TestConstants:
    def test_min_relaunch_positive(self):
        assert MIN_RELAUNCH_INTERVAL_S > 0

    def test_window_larger_than_interval(self):
        assert LAUNCH_WINDOW_S > MIN_RELAUNCH_INTERVAL_S

    def test_max_launches_positive(self):
        assert MAX_LAUNCHES_PER_WINDOW > 0
