"""Tests for the backfill CLI command."""

from datetime import date
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from click.testing import CliRunner

from apps.backend.worker.cli import _subtract_months, cli


class TestSubtractMonths:
    """Tests for the _subtract_months helper."""

    def test_subtract_one_month(self):
        result = _subtract_months(date(2026, 5, 15), 1)
        assert result == date(2026, 4, 15)

    def test_subtract_across_year(self):
        result = _subtract_months(date(2026, 2, 10), 3)
        assert result == date(2025, 11, 10)

    def test_subtract_multiple_years(self):
        result = _subtract_months(date(2026, 5, 6), 24)
        assert result == date(2024, 5, 6)

    def test_clamp_day_to_month_length(self):
        result = _subtract_months(date(2026, 3, 31), 1)
        assert result == date(2026, 2, 28)  # Feb has 28 days in 2026

    def test_leap_year_february(self):
        result = _subtract_months(date(2024, 3, 31), 1)
        assert result == date(2024, 2, 29)  # 2024 is a leap year

    def test_zero_months(self):
        result = _subtract_months(date(2026, 5, 6), 0)
        assert result == date(2026, 5, 6)


class TestBackfillCommand:
    """Tests for the 'backfill' CLI command."""

    @pytest.fixture(autouse=True)
    def _mock_deps(self):
        """Mock heavy dependencies (DB, ingestion, recompute)."""
        with (
            patch("apps.backend.worker.cli.get_session") as mock_get_session,
            patch("apps.backend.worker.cli.IngestionService") as mock_ingestion,
            patch(
                "packages.core.ratings.recompute.recompute_ratings"
            ) as mock_recompute,
        ):
            # Session context manager mock
            mock_session = MagicMock()
            mock_get_session.return_value.__enter__.return_value = mock_session
            mock_get_session.return_value.__exit__.return_value = None

            # IngestionService mock
            mock_service_instance = MagicMock()
            mock_service_instance.ingest_date_range = AsyncMock(
                return_value=(10, 100, 500)
            )
            mock_service_instance.stats = {"errors": 0}
            mock_ingestion.return_value = mock_service_instance

            # recompute_ratings mock
            mock_recompute.return_value = 5000

            yield {
                "get_session": mock_get_session,
                "ingestion": mock_ingestion,
                "recompute": mock_recompute,
                "session": mock_session,
                "service_instance": mock_service_instance,
            }

    def test_backfill_defaults(self, _mock_deps):
        """Test backfill with default options (--skip-eval)."""
        runner = CliRunner()
        result = runner.invoke(cli, ["backfill", "--skip-eval"])

        assert (
            result.exit_code == 0
        ), f"CLI exited with code {result.exit_code}: {result.output}"
        assert "Backfill" in result.output
        assert "5000" in result.output  # snapshot count from mock

    def test_backfill_with_all_options(self, _mock_deps):
        """Test backfill with all options explicitly set."""
        runner = CliRunner()
        result = runner.invoke(
            cli,
            [
                "backfill",
                "--months",
                "6",
                "--category",
                "T",
                "--source",
                "tab",
                "--clear",
                "--learn-adjustments",
                "--skip-eval",
            ],
        )

        assert (
            result.exit_code == 0
        ), f"CLI exited with code {result.exit_code}: {result.output}"

        # Verify recompute_ratings was called with correct keyword args
        _mock_deps["recompute"].assert_called_once()
        _args, kwargs = _mock_deps["recompute"].call_args
        assert kwargs.get("clear_existing") is True
        assert kwargs.get("learn_adjustments") is True

    @pytest.mark.parametrize("months", [1, 12, 24])
    def test_backfill_valid_months(self, _mock_deps, months):
        """Test various valid month values."""
        runner = CliRunner()
        result = runner.invoke(
            cli, ["backfill", "--months", str(months), "--skip-eval"]
        )
        assert (
            result.exit_code == 0
        ), f"CLI exited with code {result.exit_code}: {result.output}"

    def test_backfill_invalid_months_out_of_range(self, _mock_deps):
        """Test months outside 1-24 are rejected by Click."""
        runner = CliRunner()
        result = runner.invoke(cli, ["backfill", "--months", "25"])
        assert result.exit_code != 0
        assert "Invalid value" in result.output

    def test_backfill_invalid_months_zero(self, _mock_deps):
        """Test months=0 is rejected by Click IntRange."""
        runner = CliRunner()
        result = runner.invoke(cli, ["backfill", "--months", "0"])
        assert result.exit_code != 0
        assert "Invalid value" in result.output

    def test_backfill_with_category_h(self, _mock_deps):
        """Test with harness category."""
        runner = CliRunner()
        result = runner.invoke(cli, ["backfill", "--category", "H", "--skip-eval"])
        assert result.exit_code == 0

    def test_backfill_with_source_ingest(self, _mock_deps):
        """Test with ingest source."""
        runner = CliRunner()
        result = runner.invoke(cli, ["backfill", "--source", "ingest", "--skip-eval"])
        assert result.exit_code == 0

    def test_backfill_ingestion_error_handling(self, _mock_deps):
        """Test that individual week ingestion failures are handled gracefully."""
        svc = _mock_deps["service_instance"]
        # First call succeeds, second fails, third succeeds
        svc.ingest_date_range = AsyncMock(
            side_effect=[
                (5, 50, 200),  # week 1 OK
                Exception("API Timeout"),  # week 2 fails
                (8, 80, 400),  # week 3 OK
            ]
        )

        runner = CliRunner()
        result = runner.invoke(cli, ["backfill", "--months", "1", "--skip-eval"])

        # Command should continue despite errors
        assert (
            result.exit_code == 0
        ), f"Should handle errors gracefully: {result.output}"
        # Should have attempted multiple weeks
        assert svc.ingest_date_range.call_count >= 2

    def test_backfill_all_weeks_fail(self, _mock_deps):
        """Test when all ingestion weeks fail, recompute is skipped."""
        svc = _mock_deps["service_instance"]
        svc.ingest_date_range = AsyncMock(side_effect=Exception("Total failure"))

        runner = CliRunner()
        result = runner.invoke(cli, ["backfill", "--months", "1", "--skip-eval"])

        assert result.exit_code == 0
        # recompute should NOT be called since no meetings were ingested
        _mock_deps["recompute"].assert_not_called()
        assert "skipping recompute" in result.output.lower()

    def test_backfill_recompute_failure(self, _mock_deps):
        """Test that recompute failure exits with error."""
        _mock_deps["recompute"].side_effect = Exception("Recompute error")

        runner = CliRunner()
        result = runner.invoke(cli, ["backfill", "--months", "1", "--skip-eval"])

        assert result.exit_code != 0
        assert "Recompute failed" in result.output

    def test_backfill_ingestion_service_created_with_source(self, _mock_deps):
        """Test IngestionService is created with the correct source."""
        runner = CliRunner()
        result = runner.invoke(
            cli,
            ["backfill", "--source", "ingest", "--months", "1", "--skip-eval"],
        )

        assert result.exit_code == 0
        # Check IngestionService was created with source="ingest"
        _mock_deps["ingestion"].assert_called()
        _call_args, _call_kwargs = _mock_deps["ingestion"].call_args
        # The second positional arg or keyword arg should be source="ingest"
        source_arg = _call_kwargs.get("source")
        # It could also be a positional arg
        if source_arg is None and len(_call_args) >= 2:
            source_arg = _call_args[1]
        assert source_arg == "ingest"


class TestBackfillHelp:
    """Tests for backfill help text."""

    def test_backfill_help(self):
        """Test backfill command help output."""
        runner = CliRunner()
        result = runner.invoke(cli, ["backfill", "--help"])
        assert result.exit_code == 0
        assert "Backfill historical data" in result.output
        assert "--months" in result.output
        assert "--category" in result.output
        assert "--source" in result.output
        assert "--clear" in result.output
        assert "--learn-adjustments" in result.output
        assert "--skip-eval" in result.output
