"""Tests for glreview.review_state module."""

from __future__ import annotations

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

import pytest

from glreview.config import Config
from glreview.registry import ModuleStatus, Registry
from glreview.review_state import (
    ModuleStateInfo,
    RegistryStateInfo,
    ReviewStateChecker,
)


class TestModuleStateInfo:
    """Tests for ModuleStateInfo dataclass."""

    def test_default_values(self):
        """Default values are correct."""
        module = ModuleStatus(path="src/main.py")
        info = ModuleStateInfo(module=module)
        assert info.module == module
        assert info.is_stale is False
        assert info.stale_error is None

    def test_effective_status_not_stale(self):
        """effective_status returns module status when not stale."""
        module = ModuleStatus(path="src/main.py", status="reviewed")
        info = ModuleStateInfo(module=module, is_stale=False)
        assert info.effective_status == "reviewed"

    def test_effective_status_stale(self):
        """effective_status returns needs_re_review when stale."""
        module = ModuleStatus(path="src/main.py", status="reviewed")
        info = ModuleStateInfo(module=module, is_stale=True)
        assert info.effective_status == "needs_re_review"

    def test_effective_status_non_reviewed(self):
        """effective_status for non-reviewed is unchanged even if stale."""
        module = ModuleStatus(path="src/main.py", status="in_progress")
        info = ModuleStateInfo(module=module, is_stale=True)
        assert info.effective_status == "in_progress"


class TestRegistryStateInfo:
    """Tests for RegistryStateInfo dataclass."""

    def test_total(self):
        """total returns sum of all categories."""
        state = RegistryStateInfo(
            in_progress=[MagicMock() for _ in range(2)],
            needs_review=[MagicMock() for _ in range(3)],
            reviewed_current=[MagicMock() for _ in range(4)],
            reviewed_stale=[MagicMock() for _ in range(1)],
        )
        assert state.total == 10

    def test_reviewed_count(self):
        """reviewed_count returns only current reviewed."""
        state = RegistryStateInfo(
            in_progress=[],
            needs_review=[],
            reviewed_current=[MagicMock() for _ in range(4)],
            reviewed_stale=[MagicMock() for _ in range(2)],
        )
        assert state.reviewed_count == 4

    def test_total_lines(self):
        """total_lines sums lines from all modules."""
        mod1 = ModuleStatus(path="a.py", lines=100)
        mod2 = ModuleStatus(path="b.py", lines=50)
        mod3 = ModuleStatus(path="c.py", lines=200)
        state = RegistryStateInfo(
            in_progress=[ModuleStateInfo(module=mod1)],
            needs_review=[ModuleStateInfo(module=mod2)],
            reviewed_current=[ModuleStateInfo(module=mod3)],
            reviewed_stale=[],
        )
        assert state.total_lines == 350

    def test_reviewed_lines(self):
        """reviewed_lines sums only current reviewed."""
        mod1 = ModuleStatus(path="a.py", status="reviewed", lines=100)
        mod2 = ModuleStatus(path="b.py", status="reviewed", lines=50)
        mod3 = ModuleStatus(path="c.py", status="needs_review", lines=200)
        state = RegistryStateInfo(
            in_progress=[],
            needs_review=[ModuleStateInfo(module=mod3)],
            reviewed_current=[
                ModuleStateInfo(module=mod1),
                ModuleStateInfo(module=mod2),
            ],
            reviewed_stale=[],
        )
        assert state.reviewed_lines == 150


class TestReviewStateChecker:
    """Tests for ReviewStateChecker class."""

    @pytest.fixture
    def config(self, temp_dir):
        """Create a config with temp_dir as root."""
        return Config(root=temp_dir)

    @pytest.fixture
    def checker(self, config):
        """Create a ReviewStateChecker."""
        return ReviewStateChecker(config)

    def test_is_module_stale_not_reviewed(self, checker):
        """Non-reviewed modules are not stale."""
        module = ModuleStatus(path="src/main.py", status="in_progress")
        is_stale, error = checker.is_module_stale(module)
        assert is_stale is False
        assert error is None

    def test_is_module_stale_no_commit(self, checker):
        """Reviewed without commit is stale."""
        module = ModuleStatus(
            path="src/main.py",
            status="reviewed",
            last_reviewed_commit=None,
        )
        is_stale, error = checker.is_module_stale(module)
        assert is_stale is True
        assert "No review commit recorded" in error

    @patch("glreview.review_state.has_changes_since")
    def test_is_module_stale_changed(self, mock_has_changes, checker):
        """Reviewed module that changed is stale."""
        mock_has_changes.return_value = True
        module = ModuleStatus(
            path="src/main.py",
            status="reviewed",
            last_reviewed_commit="abc123",
        )
        is_stale, error = checker.is_module_stale(module)
        assert is_stale is True
        assert error is None

    @patch("glreview.review_state.has_changes_since")
    def test_is_module_stale_not_changed(self, mock_has_changes, checker):
        """Reviewed module that hasn't changed is not stale."""
        mock_has_changes.return_value = False
        module = ModuleStatus(
            path="src/main.py",
            status="reviewed",
            last_reviewed_commit="abc123",
        )
        is_stale, error = checker.is_module_stale(module)
        assert is_stale is False
        assert error is None

    @patch("glreview.review_state.has_changes_since")
    def test_is_module_stale_git_error(self, mock_has_changes, checker):
        """Git error makes module stale with error message."""
        mock_has_changes.side_effect = Exception("git error")
        module = ModuleStatus(
            path="src/main.py",
            status="reviewed",
            last_reviewed_commit="abc123",
        )
        is_stale, error = checker.is_module_stale(module)
        assert is_stale is True
        assert "git error" in error

    @patch("glreview.review_state.has_changes_since")
    def test_get_module_state(self, mock_has_changes, checker):
        """get_module_state returns ModuleStateInfo."""
        mock_has_changes.return_value = True
        module = ModuleStatus(
            path="src/main.py",
            status="reviewed",
            last_reviewed_commit="abc123",
        )
        info = checker.get_module_state(module)
        assert info.module == module
        assert info.is_stale is True
        assert info.stale_error is None

    @patch("glreview.review_state.has_changes_since")
    def test_get_registry_state(self, mock_has_changes, checker):
        """get_registry_state categorizes modules correctly."""
        mock_has_changes.return_value = False

        registry = Registry()
        registry.set(
            ModuleStatus(path="a.py", status="in_progress")
        )
        registry.set(
            ModuleStatus(path="b.py", status="needs_review")
        )
        registry.set(
            ModuleStatus(
                path="c.py",
                status="reviewed",
                last_reviewed_commit="abc123",
            )
        )

        state = checker.get_registry_state(registry)

        assert len(state.in_progress) == 1
        assert state.in_progress[0].module.path == "a.py"

        assert len(state.needs_review) == 1
        assert state.needs_review[0].module.path == "b.py"

        assert len(state.reviewed_current) == 1
        assert state.reviewed_current[0].module.path == "c.py"

        assert len(state.reviewed_stale) == 0

    @patch("glreview.review_state.has_changes_since")
    def test_get_registry_state_stale(self, mock_has_changes, checker):
        """get_registry_state detects stale modules."""
        mock_has_changes.return_value = True  # All reviewed are stale

        registry = Registry()
        registry.set(
            ModuleStatus(
                path="a.py",
                status="reviewed",
                last_reviewed_commit="abc123",
            )
        )

        state = checker.get_registry_state(registry)

        assert len(state.reviewed_current) == 0
        assert len(state.reviewed_stale) == 1
        assert state.reviewed_stale[0].module.path == "a.py"


class TestExtractIssueNumber:
    """Tests for extract_issue_number static method."""

    def test_none_input(self):
        """Returns None for None input."""
        assert ReviewStateChecker.extract_issue_number(None) is None

    def test_just_number(self):
        """Extracts plain number."""
        assert ReviewStateChecker.extract_issue_number("123") == "123"

    def test_hash_prefix(self):
        """Strips # prefix."""
        assert ReviewStateChecker.extract_issue_number("#123") == "123"

    def test_gitlab_url(self):
        """Extracts from GitLab URL."""
        url = "https://gitlab.com/org/project/-/issues/123"
        assert ReviewStateChecker.extract_issue_number(url) == "123"

    def test_gitlab_url_trailing_slash(self):
        """Handles trailing slash in URL."""
        url = "https://gitlab.com/org/project/-/issues/123/"
        assert ReviewStateChecker.extract_issue_number(url) == "123"

    def test_whitespace_stripped(self):
        """Strips whitespace."""
        assert ReviewStateChecker.extract_issue_number("  #123  ") == "123"


class TestFormatIssueLink:
    """Tests for format_issue_link static method."""

    def test_none_input(self):
        """Returns dash for None."""
        assert ReviewStateChecker.format_issue_link(None) == "-"

    def test_empty_string(self):
        """Returns dash for empty string."""
        assert ReviewStateChecker.format_issue_link("") == "-"

    def test_just_number(self):
        """Adds # prefix to number."""
        assert ReviewStateChecker.format_issue_link("123") == "#123"

    def test_hash_prefix(self):
        """Keeps # prefix."""
        assert ReviewStateChecker.format_issue_link("#123") == "#123"

    def test_gitlab_url(self):
        """Creates markdown link from URL."""
        url = "https://gitlab.com/org/project/-/issues/123"
        result = ReviewStateChecker.format_issue_link(url)
        assert result == f"[#123]({url})"

    def test_non_http_path(self):
        """Non-http path with slash is treated as number."""
        # Edge case: path without http://
        result = ReviewStateChecker.format_issue_link("org/project/123")
        assert result == "#org/project/123"
