"""Tests for the Golden Path generator."""

import json
from datetime import UTC, datetime
from unittest.mock import MagicMock, patch

from timmy_automations.daily_run.golden_path import (
    TIME_ESTIMATES,
    TYPE_PATTERNS,
    GiteaClient,
    GoldenPath,
    PathItem,
    build_golden_path,
    classify_issue_type,
    estimate_time,
    extract_size,
    generate_golden_path,
    get_token,
    group_issues_by_type,
    load_config,
    score_issue_for_path,
)


class TestLoadConfig:
    """Tests for configuration loading."""

    def test_load_config_defaults(self):
        """Config should have sensible defaults."""
        config = load_config()
        assert "gitea_api" in config
        assert "repo_slug" in config
        assert "size_labels" in config

    def test_load_config_env_override(self, monkeypatch):
        """Environment variables should override defaults."""
        monkeypatch.setenv("TIMMY_GITEA_API", "http://custom:3000/api/v1")
        monkeypatch.setenv("TIMMY_REPO_SLUG", "custom/repo")
        monkeypatch.setenv("TIMMY_GITEA_TOKEN", "test-token")

        config = load_config()
        assert config["gitea_api"] == "http://custom:3000/api/v1"
        assert config["repo_slug"] == "custom/repo"
        assert config["token"] == "test-token"


class TestGetToken:
    """Tests for token retrieval."""

    def test_get_token_from_config(self):
        """Token from config takes precedence."""
        config = {"token": "config-token", "token_file": "~/.test"}
        assert get_token(config) == "config-token"

    @patch("pathlib.Path.exists")
    @patch("pathlib.Path.read_text")
    def test_get_token_from_file(self, mock_read, mock_exists):
        """Token can be read from file."""
        mock_exists.return_value = True
        mock_read.return_value = "file-token\n"

        config = {"token_file": "~/.hermes/test_token"}
        assert get_token(config) == "file-token"

    def test_get_token_none(self):
        """Returns None if no token available."""
        config = {"token_file": "/nonexistent/path"}
        assert get_token(config) is None


class TestExtractSize:
    """Tests for size label extraction."""

    def test_extract_size_xs(self):
        """Should extract XS size."""
        labels = [{"name": "size:XS"}, {"name": "bug"}]
        assert extract_size(labels) == "XS"

    def test_extract_size_s(self):
        """Should extract S size."""
        labels = [{"name": "bug"}, {"name": "size:S"}]
        assert extract_size(labels) == "S"

    def test_extract_size_m(self):
        """Should extract M size."""
        labels = [{"name": "size:M"}]
        assert extract_size(labels) == "M"

    def test_extract_size_unknown(self):
        """Should return ? for unknown size."""
        labels = [{"name": "bug"}, {"name": "feature"}]
        assert extract_size(labels) == "?"

    def test_extract_size_empty(self):
        """Should return ? for empty labels."""
        assert extract_size([]) == "?"


class TestClassifyIssueType:
    """Tests for issue type classification."""

    def test_classify_triage(self):
        """Should classify triage issues."""
        issue = {
            "title": "Triage new issues",
            "labels": [{"name": "triage"}],
        }
        assert classify_issue_type(issue) == "triage"

    def test_classify_test(self):
        """Should classify test issues."""
        issue = {
            "title": "Add unit tests for parser",
            "labels": [{"name": "test"}],
        }
        assert classify_issue_type(issue) == "test"

    def test_classify_fix(self):
        """Should classify fix issues."""
        issue = {
            "title": "Fix login bug",
            "labels": [{"name": "bug"}],
        }
        assert classify_issue_type(issue) == "fix"

    def test_classify_docs(self):
        """Should classify docs issues."""
        issue = {
            "title": "Update README",
            "labels": [{"name": "docs"}],
        }
        assert classify_issue_type(issue) == "docs"

    def test_classify_refactor(self):
        """Should classify refactor issues."""
        issue = {
            "title": "Refactor validation logic",
            "labels": [{"name": "refactor"}],
        }
        assert classify_issue_type(issue) == "refactor"

    def test_classify_default_to_fix(self):
        """Should default to fix for uncategorized."""
        issue = {
            "title": "Something vague",
            "labels": [{"name": "question"}],
        }
        assert classify_issue_type(issue) == "fix"

    def test_classify_title_priority(self):
        """Title patterns should contribute to classification."""
        issue = {
            "title": "Fix the broken parser",
            "labels": [],
        }
        assert classify_issue_type(issue) == "fix"


class TestEstimateTime:
    """Tests for time estimation."""

    def test_estimate_xs_fix(self):
        """XS fix should be 10 minutes."""
        issue = {
            "title": "Fix typo",
            "labels": [{"name": "size:XS"}, {"name": "bug"}],
        }
        assert estimate_time(issue) == 10

    def test_estimate_s_test(self):
        """S test should be 15 minutes."""
        issue = {
            "title": "Add test coverage",
            "labels": [{"name": "size:S"}, {"name": "test"}],
        }
        assert estimate_time(issue) == 15

    def test_estimate_m_fix(self):
        """M fix should be 25 minutes."""
        issue = {
            "title": "Fix complex bug",
            "labels": [{"name": "size:M"}, {"name": "bug"}],
        }
        assert estimate_time(issue) == 25

    def test_estimate_unknown_size(self):
        """Unknown size should fallback to S."""
        issue = {
            "title": "Some fix",
            "labels": [{"name": "bug"}],
        }
        # Falls back to S/fix = 15
        assert estimate_time(issue) == 15


class TestScoreIssueForPath:
    """Tests for issue scoring."""

    def test_score_prefers_xs(self):
        """XS issues should score higher."""
        xs = {"title": "Fix", "labels": [{"name": "size:XS"}]}
        s = {"title": "Fix", "labels": [{"name": "size:S"}]}
        m = {"title": "Fix", "labels": [{"name": "size:M"}]}

        assert score_issue_for_path(xs) > score_issue_for_path(s)
        assert score_issue_for_path(s) > score_issue_for_path(m)

    def test_score_prefers_clear_types(self):
        """Issues with clear type labels score higher."""
        # Bug label adds score, so with bug should be >= without bug
        with_type = {
            "title": "Fix bug",
            "labels": [{"name": "size:S"}, {"name": "bug"}],
        }
        without_type = {
            "title": "Something",
            "labels": [{"name": "size:S"}],
        }

        assert score_issue_for_path(with_type) >= score_issue_for_path(without_type)

    def test_score_accepts_criteria(self):
        """Issues with acceptance criteria score higher."""
        with_criteria = {
            "title": "Fix",
            "labels": [{"name": "size:S"}],
            "body": "## Acceptance Criteria\n- [ ] Fix it",
        }
        without_criteria = {
            "title": "Fix",
            "labels": [{"name": "size:S"}],
            "body": "Just fix it",
        }

        assert score_issue_for_path(with_criteria) > score_issue_for_path(without_criteria)


class TestGroupIssuesByType:
    """Tests for issue grouping."""

    def test_groups_by_type(self):
        """Issues should be grouped by their type."""
        issues = [
            {"title": "Fix bug", "labels": [{"name": "bug"}], "number": 1},
            {"title": "Add test", "labels": [{"name": "test"}], "number": 2},
            {"title": "Another fix", "labels": [{"name": "bug"}], "number": 3},
        ]

        grouped = group_issues_by_type(issues)

        assert len(grouped["fix"]) == 2
        assert len(grouped["test"]) == 1
        assert len(grouped["triage"]) == 0

    def test_sorts_by_score(self):
        """Issues within groups should be sorted by score."""
        issues = [
            {"title": "Fix", "labels": [{"name": "size:M"}], "number": 1},
            {"title": "Fix", "labels": [{"name": "size:XS"}], "number": 2},
            {"title": "Fix", "labels": [{"name": "size:S"}], "number": 3},
        ]

        grouped = group_issues_by_type(issues)

        # XS should be first (highest score)
        assert grouped["fix"][0]["number"] == 2
        # M should be last (lowest score)
        assert grouped["fix"][2]["number"] == 1


class TestBuildGoldenPath:
    """Tests for Golden Path building."""

    def test_builds_path_with_all_types(self):
        """Path should include items from different types."""
        grouped = {
            "triage": [
                {"title": "Triage", "labels": [{"name": "size:XS"}], "number": 1, "html_url": ""},
            ],
            "fix": [
                {"title": "Fix 1", "labels": [{"name": "size:S"}], "number": 2, "html_url": ""},
                {"title": "Fix 2", "labels": [{"name": "size:XS"}], "number": 3, "html_url": ""},
            ],
            "test": [
                {"title": "Test", "labels": [{"name": "size:S"}], "number": 4, "html_url": ""},
            ],
            "docs": [],
            "refactor": [],
        }

        path = build_golden_path(grouped, target_minutes=45)

        assert path.item_count >= 3
        assert path.items[0].issue_type == "triage"  # Warm-up
        assert any(item.issue_type == "test" for item in path.items)

    def test_respects_time_budget(self):
        """Path should stay within reasonable time budget."""
        grouped = {
            "triage": [
                {"title": "Triage", "labels": [{"name": "size:S"}], "number": 1, "html_url": ""},
            ],
            "fix": [
                {"title": "Fix 1", "labels": [{"name": "size:S"}], "number": 2, "html_url": ""},
                {"title": "Fix 2", "labels": [{"name": "size:S"}], "number": 3, "html_url": ""},
            ],
            "test": [
                {"title": "Test", "labels": [{"name": "size:S"}], "number": 4, "html_url": ""},
            ],
            "docs": [],
            "refactor": [],
        }

        path = build_golden_path(grouped, target_minutes=45)

        # Should be in 30-60 minute range
        assert 20 <= path.total_estimated_minutes <= 70

    def test_no_duplicate_issues(self):
        """Path should not include the same issue twice."""
        grouped = {
            "triage": [],
            "fix": [
                {"title": "Fix", "labels": [{"name": "size:S"}], "number": 1, "html_url": ""},
            ],
            "test": [],
            "docs": [],
            "refactor": [],
        }

        path = build_golden_path(grouped, target_minutes=45)

        numbers = [item.number for item in path.items]
        assert len(numbers) == len(set(numbers))  # No duplicates

    def test_fallback_when_triage_missing(self):
        """Should use fallback when no triage issues available."""
        grouped = {
            "triage": [],
            "fix": [
                {"title": "Fix", "labels": [{"name": "size:XS"}], "number": 1, "html_url": ""},
            ],
            "test": [
                {"title": "Test", "labels": [{"name": "size:XS"}], "number": 2, "html_url": ""},
            ],
            "docs": [],
            "refactor": [],
        }

        path = build_golden_path(grouped, target_minutes=45)

        assert path.item_count > 0


class TestGoldenPathDataclass:
    """Tests for the GoldenPath dataclass."""

    def test_total_time_calculation(self):
        """Should sum item times correctly."""
        path = GoldenPath(
            generated_at=datetime.now(UTC).isoformat(),
            target_minutes=45,
            items=[
                PathItem(1, "Test 1", "XS", "fix", 10, ""),
                PathItem(2, "Test 2", "S", "test", 15, ""),
            ],
        )

        assert path.total_estimated_minutes == 25

    def test_to_dict(self):
        """Should convert to dict correctly."""
        path = GoldenPath(
            generated_at="2024-01-01T00:00:00+00:00",
            target_minutes=45,
            items=[PathItem(1, "Test", "XS", "fix", 10, "http://test")],
        )

        data = path.to_dict()

        assert data["target_minutes"] == 45
        assert data["total_estimated_minutes"] == 10
        assert data["item_count"] == 1
        assert len(data["items"]) == 1

    def test_to_json(self):
        """Should convert to JSON correctly."""
        path = GoldenPath(
            generated_at="2024-01-01T00:00:00+00:00",
            target_minutes=45,
            items=[],
        )

        json_str = path.to_json()
        data = json.loads(json_str)

        assert data["target_minutes"] == 45


class TestGiteaClient:
    """Tests for the GiteaClient."""

    def test_client_initialization(self):
        """Client should initialize with config."""
        config = {
            "gitea_api": "http://test:3000/api/v1",
            "repo_slug": "test/repo",
        }
        client = GiteaClient(config, "token123")

        assert client.api_base == "http://test:3000/api/v1"
        assert client.repo_slug == "test/repo"
        assert client.token == "token123"

    def test_headers_with_token(self):
        """Headers should include auth token."""
        config = {"gitea_api": "http://test", "repo_slug": "test/repo"}
        client = GiteaClient(config, "mytoken")

        headers = client._headers()

        assert headers["Authorization"] == "token mytoken"
        assert headers["Accept"] == "application/json"

    def test_headers_without_token(self):
        """Headers should work without token."""
        config = {"gitea_api": "http://test", "repo_slug": "test/repo"}
        client = GiteaClient(config, None)

        headers = client._headers()

        assert "Authorization" not in headers
        assert headers["Accept"] == "application/json"

    @patch("timmy_automations.daily_run.golden_path.urlopen")
    def test_is_available_success(self, mock_urlopen):
        """Should detect API availability."""
        mock_response = MagicMock()
        mock_response.status = 200
        mock_context = MagicMock()
        mock_context.__enter__ = MagicMock(return_value=mock_response)
        mock_context.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_context

        config = {"gitea_api": "http://test", "repo_slug": "test/repo"}
        client = GiteaClient(config, None)

        assert client.is_available() is True

    @patch("urllib.request.urlopen")
    def test_is_available_failure(self, mock_urlopen):
        """Should handle API unavailability."""
        from urllib.error import URLError

        mock_urlopen.side_effect = URLError("Connection refused")

        config = {"gitea_api": "http://test", "repo_slug": "test/repo"}
        client = GiteaClient(config, None)

        assert client.is_available() is False


class TestIntegration:
    """Integration-style tests."""

    @patch("timmy_automations.daily_run.golden_path.GiteaClient")
    def test_generate_golden_path_integration(self, mock_client_class):
        """End-to-end test with mocked Gitea."""
        # Setup mock
        mock_client = MagicMock()
        mock_client.is_available.return_value = True
        mock_client.get_paginated.return_value = [
            {
                "number": 1,
                "title": "Triage issues",
                "labels": [{"name": "size:XS"}, {"name": "triage"}],
                "html_url": "http://test/1",
            },
            {
                "number": 2,
                "title": "Fix bug",
                "labels": [{"name": "size:S"}, {"name": "bug"}],
                "html_url": "http://test/2",
            },
            {
                "number": 3,
                "title": "Add tests",
                "labels": [{"name": "size:S"}, {"name": "test"}],
                "html_url": "http://test/3",
            },
            {
                "number": 4,
                "title": "Another fix",
                "labels": [{"name": "size:XS"}, {"name": "bug"}],
                "html_url": "http://test/4",
            },
        ]
        mock_client_class.return_value = mock_client

        path = generate_golden_path(target_minutes=45)

        assert path.item_count >= 3
        assert all(item.url.startswith("http://test/") for item in path.items)

    @patch("timmy_automations.daily_run.golden_path.GiteaClient")
    def test_generate_when_unavailable(self, mock_client_class):
        """Should return empty path when Gitea unavailable."""
        mock_client = MagicMock()
        mock_client.is_available.return_value = False
        mock_client_class.return_value = mock_client

        path = generate_golden_path(target_minutes=45)

        assert path.item_count == 0
        assert path.items == []


class TestTypePatterns:
    """Tests for type pattern definitions."""

    def test_type_patterns_structure(self):
        """Type patterns should have required keys."""
        for _issue_type, patterns in TYPE_PATTERNS.items():
            assert "labels" in patterns
            assert "title" in patterns
            assert isinstance(patterns["labels"], list)
            assert isinstance(patterns["title"], list)

    def test_time_estimates_structure(self):
        """Time estimates should have all sizes."""
        for size in ["XS", "S", "M"]:
            assert size in TIME_ESTIMATES
            for issue_type in ["triage", "fix", "test", "docs", "refactor"]:
                assert issue_type in TIME_ESTIMATES[size]
                assert isinstance(TIME_ESTIMATES[size][issue_type], int)
                assert TIME_ESTIMATES[size][issue_type] > 0
