"""Tests for CostTracker: persistent storage and aggregate reporting."""

import json
import warnings
import pytest
from pathlib import Path
from datetime import datetime, timezone, timedelta
from typing import Optional

import agentbudget.tracker as tracker_module
from agentbudget.tracker import CostTracker


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture()
def patched_tracker(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> CostTracker:
    """Return a CostTracker whose storage is redirected to a temp directory."""
    monkeypatch.setattr(tracker_module.CostTracker, "_STORAGE_DIR", tmp_path)
    monkeypatch.setattr(tracker_module.CostTracker, "_STORAGE_FILE", tmp_path / "sessions.json")
    return CostTracker()


def make_session(
    name: str = "test-session",
    total_cost: float = 0.01,
    total_tokens: int = 100,
    steps: Optional[list] = None,
    days_ago: int = 0,
) -> dict:
    """Helper that builds a minimal session dict for storage tests."""
    ts = datetime.now(timezone.utc) - timedelta(days=days_ago)
    return {
        "id": "test-id",
        "name": name,
        "timestamp": ts.isoformat().replace("+00:00", "Z"),
        "budget": None,
        "total_cost": total_cost,
        "total_tokens": total_tokens,
        "steps": steps or [
            {
                "label": "step-1",
                "model": "gpt-4o",
                "provider": "openai",
                "input_tokens": 80,
                "output_tokens": 20,
                "cost": total_cost,
            }
        ],
    }


# ---------------------------------------------------------------------------
# save_session and _load
# ---------------------------------------------------------------------------

class TestCostTrackerSaveAndLoad:
    def test_save_session_creates_file(self, patched_tracker: CostTracker, tmp_path: Path) -> None:
        session_data = make_session(name="save-test")
        patched_tracker.save_session(session_data)

        storage_file = tmp_path / "sessions.json"
        assert storage_file.exists()

    def test_save_session_writes_valid_json(self, patched_tracker: CostTracker, tmp_path: Path) -> None:
        session_data = make_session(name="json-test")
        patched_tracker.save_session(session_data)

        with open(tmp_path / "sessions.json", "r") as f:
            data = json.load(f)

        assert isinstance(data, list)
        assert len(data) == 1
        assert data[0]["name"] == "json-test"

    def test_save_multiple_sessions_appends(self, patched_tracker: CostTracker) -> None:
        patched_tracker.save_session(make_session(name="first"))
        patched_tracker.save_session(make_session(name="second"))

        sessions = patched_tracker._load()
        assert len(sessions) == 2
        assert sessions[0]["name"] == "first"
        assert sessions[1]["name"] == "second"

    def test_load_returns_empty_list_when_no_file(self, patched_tracker: CostTracker) -> None:
        sessions = patched_tracker._load()
        assert sessions == []

    def test_load_returns_list(self, patched_tracker: CostTracker) -> None:
        patched_tracker.save_session(make_session())
        sessions = patched_tracker._load()
        assert isinstance(sessions, list)

    def test_load_preserves_all_fields(self, patched_tracker: CostTracker) -> None:
        session = make_session(name="full-fields", total_cost=0.05, total_tokens=500)
        patched_tracker.save_session(session)

        loaded = patched_tracker._load()
        assert loaded[0]["name"] == "full-fields"
        assert loaded[0]["total_cost"] == 0.05
        assert loaded[0]["total_tokens"] == 500


# ---------------------------------------------------------------------------
# _filter_sessions
# ---------------------------------------------------------------------------

class TestCostTrackerFilterSessions:
    def test_none_last_returns_all(self, patched_tracker: CostTracker) -> None:
        sessions = [make_session(days_ago=0), make_session(days_ago=30), make_session(days_ago=365)]
        result = patched_tracker._filter_sessions(sessions, last=None)
        assert len(result) == 3

    def test_last_7_excludes_old_sessions(self, patched_tracker: CostTracker) -> None:
        recent = make_session(name="recent", days_ago=3)
        old = make_session(name="old", days_ago=10)
        result = patched_tracker._filter_sessions([recent, old], last=7)
        assert len(result) == 1
        assert result[0]["name"] == "recent"

    def test_last_7_includes_today(self, patched_tracker: CostTracker) -> None:
        today = make_session(name="today", days_ago=0)
        result = patched_tracker._filter_sessions([today], last=7)
        assert len(result) == 1

    def test_last_30_includes_session_at_boundary(self, patched_tracker: CostTracker) -> None:
        """Sessions from exactly 29 days ago should be included when last=30."""
        recent_enough = make_session(name="boundary", days_ago=29)
        result = patched_tracker._filter_sessions([recent_enough], last=30)
        assert len(result) == 1

    def test_last_7_excludes_session_at_8_days(self, patched_tracker: CostTracker) -> None:
        old = make_session(name="too-old", days_ago=8)
        result = patched_tracker._filter_sessions([old], last=7)
        assert len(result) == 0

    def test_unparseable_timestamp_included(self, patched_tracker: CostTracker) -> None:
        """Sessions with unparseable timestamps should still be included."""
        bad = {"id": "x", "name": "bad-ts", "timestamp": "not-a-date", "total_cost": 0.0, "total_tokens": 0, "steps": []}
        result = patched_tracker._filter_sessions([bad], last=7)
        assert len(result) == 1


# ---------------------------------------------------------------------------
# report()
# ---------------------------------------------------------------------------

class TestCostTrackerReport:
    def test_report_empty_prints_no_sessions(self, patched_tracker: CostTracker, capsys: pytest.CaptureFixture) -> None:
        patched_tracker.report()
        out = capsys.readouterr().out
        assert "No sessions recorded." in out

    def test_report_empty_still_prints_header(self, patched_tracker: CostTracker, capsys: pytest.CaptureFixture) -> None:
        patched_tracker.report()
        out = capsys.readouterr().out
        assert "agentbudget" in out

    def test_report_with_sessions_prints_aggregate(self, patched_tracker: CostTracker, capsys: pytest.CaptureFixture) -> None:
        patched_tracker.save_session(make_session(name="s1", total_cost=0.01, total_tokens=150))
        patched_tracker.save_session(make_session(name="s2", total_cost=0.02, total_tokens=300))
        patched_tracker.report()
        out = capsys.readouterr().out

        assert "Sessions:" in out
        assert "Total cost:" in out
        assert "Total tokens:" in out

    def test_report_with_sessions_shows_correct_session_count(self, patched_tracker: CostTracker, capsys: pytest.CaptureFixture) -> None:
        patched_tracker.save_session(make_session())
        patched_tracker.save_session(make_session())
        patched_tracker.report()
        out = capsys.readouterr().out
        assert "2" in out

    def test_report_with_last_filter(self, patched_tracker: CostTracker, capsys: pytest.CaptureFixture) -> None:
        recent = make_session(name="recent", days_ago=2)
        old = make_session(name="old", days_ago=30)
        patched_tracker.save_session(recent)
        patched_tracker.save_session(old)

        patched_tracker.report(last=7)
        out = capsys.readouterr().out
        # Only 1 session should appear
        assert "1" in out
        assert "last 7 days" in out

    def test_report_shows_most_expensive_step(self, patched_tracker: CostTracker, capsys: pytest.CaptureFixture) -> None:
        session = make_session(steps=[
            {"label": "expensive-step", "model": "gpt-4o", "provider": "openai", "input_tokens": 1000, "output_tokens": 500, "cost": 0.50},
        ], total_cost=0.50, total_tokens=1500)
        patched_tracker.save_session(session)
        patched_tracker.report()
        out = capsys.readouterr().out
        assert "Most expensive step" in out
        assert "expensive-step" in out

    def test_report_shows_most_used_model(self, patched_tracker: CostTracker, capsys: pytest.CaptureFixture) -> None:
        session = make_session(steps=[
            {"label": "step-1", "model": "gpt-4o", "provider": "openai", "input_tokens": 100, "output_tokens": 50, "cost": 0.01},
        ], total_cost=0.01, total_tokens=150)
        patched_tracker.save_session(session)
        patched_tracker.report()
        out = capsys.readouterr().out
        assert "Most used model" in out
        assert "gpt-4o" in out


# ---------------------------------------------------------------------------
# clear()
# ---------------------------------------------------------------------------

class TestCostTrackerClear:
    def test_clear_deletes_file(self, patched_tracker: CostTracker, tmp_path: Path) -> None:
        patched_tracker.save_session(make_session())
        assert (tmp_path / "sessions.json").exists()

        patched_tracker.clear()
        assert not (tmp_path / "sessions.json").exists()

    def test_clear_when_no_file_does_not_raise(self, patched_tracker: CostTracker) -> None:
        """Calling clear() when there is no file should be a no-op."""
        patched_tracker.clear()  # should not raise

    def test_after_clear_load_returns_empty(self, patched_tracker: CostTracker) -> None:
        patched_tracker.save_session(make_session())
        patched_tracker.clear()
        sessions = patched_tracker._load()
        assert sessions == []


# ---------------------------------------------------------------------------
# Corrupted JSON
# ---------------------------------------------------------------------------

class TestCostTrackerCorruptedJson:
    def test_corrupted_json_emits_runtime_warning(self, patched_tracker: CostTracker, tmp_path: Path) -> None:
        corrupted_file = tmp_path / "sessions.json"
        corrupted_file.write_text("NOT VALID JSON ][", encoding="utf-8")

        with pytest.warns(RuntimeWarning, match="could not be parsed"):
            sessions = patched_tracker._load()

        assert sessions == []

    def test_corrupted_json_returns_empty_list(self, patched_tracker: CostTracker, tmp_path: Path) -> None:
        corrupted_file = tmp_path / "sessions.json"
        corrupted_file.write_text("{broken", encoding="utf-8")

        with warnings.catch_warnings():
            warnings.simplefilter("ignore", RuntimeWarning)
            sessions = patched_tracker._load()

        assert sessions == []
