"""Tests for the integrity sweep script.

Covers work-item filtering, verification outcomes, and result aggregation.
All network calls are mocked -- no real Binance Vision requests.
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

# ---------------------------------------------------------------------------
# Helpers: import the module under test
# ---------------------------------------------------------------------------

# The script lives at scripts/integrity_sweep.py -- add scripts/ to sys.path
# so we can import its functions directly.
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
if str(SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPTS_DIR))


# ---------------------------------------------------------------------------
# Test _build_work_items
# ---------------------------------------------------------------------------


class TestBuildWorkItems:
    """Tests for _build_work_items() discovery + filtering."""

    def test_empty_registry_returns_all_days(self, tmp_path: Path) -> None:
        """With no prior verifications, all Parquet days become work items."""
        from integrity_sweep import _build_work_items

        # Create fake ticks directory with two symbols
        ticks_dir = tmp_path / "ticks"
        (ticks_dir / "BTCUSDT").mkdir(parents=True)
        (ticks_dir / "BTCUSDT" / "2024-01-01.parquet").touch()
        (ticks_dir / "BTCUSDT" / "2024-01-02.parquet").touch()
        (ticks_dir / "ETHUSDT").mkdir(parents=True)
        (ticks_dir / "ETHUSDT" / "2024-01-01.parquet").touch()

        registry = MagicMock()
        registry.has_verified.return_value = False

        args = argparse.Namespace(
            full=False,
            symbol=None,
            start_date=None,
            end_date=None,
        )

        items = _build_work_items(ticks_dir, registry, args)
        assert len(items) == 3
        symbols = {item["symbol"] for item in items}
        assert symbols == {"BTCUSDT", "ETHUSDT"}

    def test_incremental_skips_verified(self, tmp_path: Path) -> None:
        """Incremental mode skips days already in the registry."""
        from integrity_sweep import _build_work_items

        ticks_dir = tmp_path / "ticks"
        (ticks_dir / "BTCUSDT").mkdir(parents=True)
        (ticks_dir / "BTCUSDT" / "2024-01-01.parquet").touch()
        (ticks_dir / "BTCUSDT" / "2024-01-02.parquet").touch()

        registry = MagicMock()
        registry.has_verified.side_effect = lambda _sym, dt: dt == "2024-01-01"

        args = argparse.Namespace(
            full=False,
            symbol=None,
            start_date=None,
            end_date=None,
        )

        items = _build_work_items(ticks_dir, registry, args)
        assert len(items) == 1
        assert items[0]["date"] == "2024-01-02"

    def test_full_flag_ignores_registry(self, tmp_path: Path) -> None:
        """--full flag returns all items regardless of registry state."""
        from integrity_sweep import _build_work_items

        ticks_dir = tmp_path / "ticks"
        (ticks_dir / "BTCUSDT").mkdir(parents=True)
        (ticks_dir / "BTCUSDT" / "2024-01-01.parquet").touch()
        (ticks_dir / "BTCUSDT" / "2024-01-02.parquet").touch()

        registry = MagicMock()
        registry.has_verified.return_value = True  # everything "verified"

        args = argparse.Namespace(
            full=True,
            symbol=None,
            start_date=None,
            end_date=None,
        )

        items = _build_work_items(ticks_dir, registry, args)
        assert len(items) == 2

    def test_symbol_filter(self, tmp_path: Path) -> None:
        """--symbol restricts to one symbol."""
        from integrity_sweep import _build_work_items

        ticks_dir = tmp_path / "ticks"
        (ticks_dir / "BTCUSDT").mkdir(parents=True)
        (ticks_dir / "BTCUSDT" / "2024-01-01.parquet").touch()
        (ticks_dir / "ETHUSDT").mkdir(parents=True)
        (ticks_dir / "ETHUSDT" / "2024-01-01.parquet").touch()

        registry = MagicMock()
        registry.has_verified.return_value = False

        args = argparse.Namespace(
            full=False,
            symbol="BTCUSDT",
            start_date=None,
            end_date=None,
        )

        items = _build_work_items(ticks_dir, registry, args)
        assert len(items) == 1
        assert items[0]["symbol"] == "BTCUSDT"

    def test_date_range_filter(self, tmp_path: Path) -> None:
        """--start-date and --end-date restrict the date range."""
        from integrity_sweep import _build_work_items

        ticks_dir = tmp_path / "ticks"
        (ticks_dir / "BTCUSDT").mkdir(parents=True)
        (ticks_dir / "BTCUSDT" / "2024-01-01.parquet").touch()
        (ticks_dir / "BTCUSDT" / "2024-01-15.parquet").touch()
        (ticks_dir / "BTCUSDT" / "2024-02-01.parquet").touch()

        registry = MagicMock()
        registry.has_verified.return_value = False

        args = argparse.Namespace(
            full=False,
            symbol=None,
            start_date="2024-01-10",
            end_date="2024-01-31",
        )

        items = _build_work_items(ticks_dir, registry, args)
        assert len(items) == 1
        assert items[0]["date"] == "2024-01-15"

    def test_prefixed_directory_names(self, tmp_path: Path) -> None:
        """Handles BINANCE_SPOT_BTCUSDT directory naming convention."""
        from integrity_sweep import _build_work_items

        ticks_dir = tmp_path / "ticks"
        (ticks_dir / "BINANCE_SPOT_BTCUSDT").mkdir(parents=True)
        (ticks_dir / "BINANCE_SPOT_BTCUSDT" / "2024-01-01.parquet").touch()

        registry = MagicMock()
        registry.has_verified.return_value = False

        args = argparse.Namespace(
            full=False,
            symbol=None,
            start_date=None,
            end_date=None,
        )

        items = _build_work_items(ticks_dir, registry, args)
        assert len(items) == 1
        # The symbol used for Vision URL should be the base symbol
        assert items[0]["symbol"] == "BTCUSDT"


# ---------------------------------------------------------------------------
# Test _verify_one_day
# ---------------------------------------------------------------------------


class TestVerifyOneDay:
    """Tests for _verify_one_day() verification outcomes."""

    @patch("integrity_sweep.requests")
    def test_matching_checksum_returns_pass(self, mock_requests: MagicMock) -> None:
        """Matching checksum returns pass status."""
        from integrity_sweep import _verify_one_day

        # Mock CHECKSUM response
        checksum_resp = MagicMock()
        checksum_resp.status_code = 200
        checksum_resp.text = "abc123def456  BTCUSDT-aggTrades-2024-01-01.zip"

        # Mock ZIP response
        zip_resp = MagicMock()
        zip_resp.status_code = 200
        zip_resp.content = b"fake zip bytes"

        mock_requests.get.side_effect = [checksum_resp, zip_resp]
        mock_requests.Timeout = Exception
        mock_requests.ConnectionError = Exception

        with patch("integrity_sweep.parse_checksum_file_content", return_value="abc123def456"), \
             patch("integrity_sweep.compute_sha256_hex", return_value="abc123def456"):
            result = _verify_one_day("BTCUSDT", "2024-01-01", timeout_s=30)

        assert result["status"] == "pass"
        assert result["sha256"] == "abc123def456"

    @patch("integrity_sweep.requests")
    def test_404_checksum_returns_skip(self, mock_requests: MagicMock) -> None:
        """404 CHECKSUM file returns skip status."""
        from integrity_sweep import _verify_one_day

        resp = MagicMock()
        resp.status_code = 404

        mock_requests.get.return_value = resp
        mock_requests.Timeout = Exception
        mock_requests.ConnectionError = Exception

        result = _verify_one_day("BTCUSDT", "2024-01-01", timeout_s=30)
        assert result["status"] == "skip"
        assert result["reason"] == "404"

    @patch("integrity_sweep.requests")
    def test_mismatch_returns_fail(self, mock_requests: MagicMock) -> None:
        """Hash mismatch returns fail status."""
        from integrity_sweep import _verify_one_day

        checksum_resp = MagicMock()
        checksum_resp.status_code = 200
        checksum_resp.text = "expected_hash  BTCUSDT-aggTrades-2024-01-01.zip"

        zip_resp = MagicMock()
        zip_resp.status_code = 200
        zip_resp.content = b"fake zip bytes"

        mock_requests.get.side_effect = [checksum_resp, zip_resp]
        mock_requests.Timeout = Exception
        mock_requests.ConnectionError = Exception

        with patch("integrity_sweep.parse_checksum_file_content", return_value="expected_hash"), \
             patch("integrity_sweep.compute_sha256_hex", return_value="actual_different_hash"):
            result = _verify_one_day("BTCUSDT", "2024-01-01", timeout_s=30)

        assert result["status"] == "fail"

    @patch("integrity_sweep.requests")
    def test_timeout_returns_skip(self, mock_requests: MagicMock) -> None:
        """Network timeout returns skip status."""
        from integrity_sweep import _verify_one_day

        mock_requests.Timeout = type("Timeout", (Exception,), {})
        mock_requests.ConnectionError = type("ConnError", (Exception,), {})
        mock_requests.get.side_effect = mock_requests.Timeout("timed out")

        result = _verify_one_day("BTCUSDT", "2024-01-01", timeout_s=30)
        assert result["status"] == "skip"
        assert result["reason"] == "timeout"


# ---------------------------------------------------------------------------
# Test _aggregate_results
# ---------------------------------------------------------------------------


class TestAggregateResults:
    """Tests for _aggregate_results() summary generation."""

    def test_per_symbol_counts(self) -> None:
        """Produces correct per-symbol pass/fail/skip counts."""
        from integrity_sweep import _aggregate_results

        results = [
            {"symbol": "BTCUSDT", "date": "2024-01-01", "status": "pass", "sha256": "abc"},
            {"symbol": "BTCUSDT", "date": "2024-01-02", "status": "pass", "sha256": "def"},
            {"symbol": "BTCUSDT", "date": "2024-01-03", "status": "fail"},
            {"symbol": "ETHUSDT", "date": "2024-01-01", "status": "skip", "reason": "404"},
            {"symbol": "ETHUSDT", "date": "2024-01-02", "status": "pass", "sha256": "ghi"},
        ]

        agg = _aggregate_results(results)

        assert agg["total"]["pass"] == 3
        assert agg["total"]["fail"] == 1
        assert agg["total"]["skip"] == 1

        assert agg["by_symbol"]["BTCUSDT"]["pass"] == 2
        assert agg["by_symbol"]["BTCUSDT"]["fail"] == 1
        assert agg["by_symbol"]["BTCUSDT"]["skip"] == 0

        assert agg["by_symbol"]["ETHUSDT"]["pass"] == 1
        assert agg["by_symbol"]["ETHUSDT"]["fail"] == 0
        assert agg["by_symbol"]["ETHUSDT"]["skip"] == 1

    def test_empty_results(self) -> None:
        """Empty results produce zero counts."""
        from integrity_sweep import _aggregate_results

        agg = _aggregate_results([])
        assert agg["total"]["pass"] == 0
        assert agg["total"]["fail"] == 0
        assert agg["total"]["skip"] == 0
        assert agg["by_symbol"] == {}

    def test_failed_items_list(self) -> None:
        """Failed items are collected in the summary."""
        from integrity_sweep import _aggregate_results

        results = [
            {"symbol": "BTCUSDT", "date": "2024-01-01", "status": "fail"},
            {"symbol": "BTCUSDT", "date": "2024-01-02", "status": "pass", "sha256": "abc"},
        ]

        agg = _aggregate_results(results)
        assert len(agg["failed"]) == 1
        assert agg["failed"][0] == ("BTCUSDT", "2024-01-01")
