"""Tests for scanner module — query building and scan orchestration."""

from __future__ import annotations

import unittest.mock
from unittest.mock import patch

from priorart.scanner import build_queries, scan

# ---------------------------------------------------------------------------
# Shared fixtures for TestScan
# ---------------------------------------------------------------------------

_MOCK_REPO = {
    "name": "org/repo",
    "stars": 100,
    "description": "A test repo",
    "url": "https://github.com/org/repo",
    "updated": "2024-01-01",
    "language": "Python",
}

_MOCK_HN_POST = {
    "title": "Test HN post",
    "points": 50,
    "comments": 5,
    "url": "https://news.ycombinator.com/item?id=1",
    "date": "2024-01-01",
}


class TestBuildQueries:
    def test_simple_idea(self):
        queries = build_queries("AI personal finance tracker")
        assert len(queries) >= 1
        assert "AI personal finance tracker" in queries

    def test_strips_idea_prefix(self):
        queries = build_queries("idea: AI personal finance tracker")
        for q in queries:
            assert not q.lower().startswith("idea:")

    def test_strips_idea_bracket_prefix(self):
        queries = build_queries("[idea] AI personal finance tracker")
        for q in queries:
            assert not q.lower().startswith("[idea]")

    def test_splits_on_dash_separator(self):
        queries = build_queries("Kuma — Self-hosted uptime monitor")
        # Should produce multi-word queries, not just "Kuma"
        assert any(len(q.split()) > 1 for q in queries)

    def test_splits_on_em_dash(self):
        queries = build_queries("FinTrack — AI finance tracker")
        assert any("AI finance tracker" in q or "finance tracker" in q for q in queries)

    def test_long_title_adds_shortened_variants(self):
        long_title = "AI-powered personal finance tracking application for mobile"
        queries = build_queries(long_title)
        # Should add 5-word and 3-word variants
        assert len(queries) >= 2

    def test_no_duplicate_queries(self):
        queries = build_queries("AI personal finance tracker")
        lower = [q.lower() for q in queries]
        assert len(lower) == len(set(lower))

    def test_body_adds_extra_query(self):
        queries = build_queries(
            "Finance app",
            body="Smart budgeting with machine learning insights",
        )
        assert any("Smart budgeting" in q or "budgeting" in q for q in queries)

    def test_body_ignores_http_lines(self):
        queries = build_queries(
            "Finance app",
            body="https://example.com/something\nSmart budgeting tool",
        )
        # URL line should be skipped
        for q in queries:
            assert "https://" not in q

    def test_returns_list_of_strings(self):
        queries = build_queries("test idea")
        assert isinstance(queries, list)
        assert all(isinstance(q, str) for q in queries)

    def test_empty_title_edge_case(self):
        # Should not raise, returns whatever makes sense
        queries = build_queries("   ")
        assert isinstance(queries, list)

    def test_strips_idea_dash_prefix(self):
        queries = build_queries("idea - AI personal finance tracker")
        for q in queries:
            assert not q.lower().startswith("idea")

    def test_pipe_separator_splits(self):
        queries = build_queries("FinTrack | AI finance tracker")
        assert any(len(q.split()) > 1 for q in queries)


class TestScan:
    """Tests for the scan() orchestrator function."""

    def _scan(
        self,
        idea="test idea",
        token="fake-token",
        sources=None,
        repos=None,
        hn=None,
        **kwargs,
    ):
        """Helper: run scan() with mocked sources and no sleeps."""
        repos = [] if repos is None else repos
        hn = [] if hn is None else hn
        sources = sources if sources is not None else ["github", "hn"]
        with patch("priorart.scanner.search_repos", return_value=repos):
            with patch("priorart.scanner.search_discussions", return_value=hn):
                with patch("priorart.scanner.time.sleep"):
                    return scan(idea, github_token=token, sources=sources, **kwargs)

    def test_returns_required_structure(self):
        result = self._scan()
        assert "query" in result
        assert "queries" in result
        assert "sources" in result
        assert "signal" in result
        assert "meta" in result

    def test_query_field_matches_input(self):
        result = self._scan(idea="my unique idea")
        assert result["query"] == "my unique idea"

    def test_queries_is_list_of_strings(self):
        result = self._scan()
        assert isinstance(result["queries"], list)
        assert all(isinstance(q, str) for q in result["queries"])

    def test_calls_github_without_token(self):
        with patch("priorart.scanner.search_repos", return_value=[]) as mock_repos:
            with patch("priorart.scanner.search_discussions", return_value=[]):
                with patch("priorart.scanner.time.sleep"):
                    scan("test idea", github_token=None, sources=["github", "hn"])
        assert mock_repos.called
        mock_repos.assert_called_with(unittest.mock.ANY, None)

    def test_calls_github_with_empty_token(self):
        with patch("priorart.scanner.search_repos", return_value=[]) as mock_repos:
            with patch("priorart.scanner.search_discussions", return_value=[]):
                with patch("priorart.scanner.time.sleep"):
                    scan("test idea", github_token="", sources=["github", "hn"])
        assert mock_repos.called

    def test_calls_github_when_token_provided(self):
        with patch("priorart.scanner.search_repos", return_value=[]) as mock_repos:
            with patch("priorart.scanner.search_discussions", return_value=[]):
                with patch("priorart.scanner.time.sleep"):
                    scan("test idea", github_token="fake-token", sources=["github"])
        assert mock_repos.called

    def test_calls_hn_when_in_sources(self):
        with patch("priorart.scanner.search_repos", return_value=[]):
            with patch(
                "priorart.scanner.search_discussions", return_value=[]
            ) as mock_hn:
                with patch("priorart.scanner.time.sleep"):
                    scan("test idea", github_token=None, sources=["hn"])
        assert mock_hn.called

    def test_skips_hn_when_not_in_sources(self):
        with patch("priorart.scanner.search_repos", return_value=[]):
            with patch("priorart.scanner.search_discussions") as mock_hn:
                with patch("priorart.scanner.time.sleep"):
                    scan("test idea", github_token="fake-token", sources=["github"])
        mock_hn.assert_not_called()

    def test_deduplicates_repos_by_name(self):
        shared = {**_MOCK_REPO, "name": "org/shared"}
        unique = {**_MOCK_REPO, "name": "org/unique"}
        call_n = [0]

        def _repos(query, token, **kw):
            call_n[0] += 1
            return [shared] if call_n[0] == 1 else [shared, unique]

        # Use a long title so build_queries produces 2+ queries
        with patch("priorart.scanner.search_repos", side_effect=_repos):
            with patch("priorart.scanner.search_discussions", return_value=[]):
                with patch("priorart.scanner.time.sleep"):
                    result = scan(
                        "AI-powered personal finance tracking application for mobile",
                        github_token="fake-token",
                        sources=["github"],
                    )

        gh_results = next(
            s["results"] for s in result["sources"] if s["type"] == "github"
        )
        names = [r["name"] for r in gh_results]
        assert names.count("org/shared") == 1

    def test_deduplicates_hn_by_url(self):
        dup = {**_MOCK_HN_POST, "url": "https://news.ycombinator.com/item?id=42"}
        # Return same post for every query
        with patch("priorart.scanner.search_repos", return_value=[]):
            with patch("priorart.scanner.search_discussions", return_value=[dup]):
                with patch("priorart.scanner.time.sleep"):
                    result = scan(
                        "AI-powered personal finance tracking application for mobile",
                        github_token=None,
                        sources=["hn"],
                    )
        hn_results = next(s["results"] for s in result["sources"] if s["type"] == "hn")
        urls = [r["url"] for r in hn_results]
        assert len(urls) == len(set(urls))

    def test_repos_sorted_by_stars_descending(self):
        low = {**_MOCK_REPO, "name": "org/low", "stars": 10}
        high = {**_MOCK_REPO, "name": "org/high", "stars": 1000}
        result = self._scan(repos=[low, high])
        gh = next(s["results"] for s in result["sources"] if s["type"] == "github")
        stars = [r["stars"] for r in gh]
        assert stars == sorted(stars, reverse=True)

    def test_hn_sorted_by_points_descending(self):
        low = {
            **_MOCK_HN_POST,
            "url": "https://news.ycombinator.com/item?id=1",
            "points": 5,
        }
        high = {
            **_MOCK_HN_POST,
            "url": "https://news.ycombinator.com/item?id=2",
            "points": 500,
        }
        result = self._scan(hn=[low, high], token=None, sources=["hn"])
        hn = next(s["results"] for s in result["sources"] if s["type"] == "hn")
        pts = [r["points"] for r in hn]
        assert pts == sorted(pts, reverse=True)

    def test_meta_timestamp_is_iso_format(self):
        result = self._scan()
        ts = result["meta"]["timestamp"]
        assert "T" in ts
        assert "+" in ts or "Z" in ts  # timezone-aware

    def test_meta_version_is_string(self):
        result = self._scan()
        assert isinstance(result["meta"]["version"], str)
        assert len(result["meta"]["version"]) > 0

    def test_source_types_reflect_requested_sources(self):
        result = self._scan(sources=["hn"], token=None)
        types = [s["type"] for s in result["sources"]]
        assert types == ["hn"]

    def test_github_source_absent_when_not_requested(self):
        result = self._scan(sources=["hn"], token=None)
        types = [s["type"] for s in result["sources"]]
        assert "github" not in types

    def test_both_source_types_present_by_default(self):
        result = self._scan()
        types = [s["type"] for s in result["sources"]]
        assert "github" in types
        assert "hn" in types

    def test_signal_score_reflects_results(self):
        repos = [
            {**_MOCK_REPO, "name": f"org/repo{i}", "stars": 5000} for i in range(8)
        ]
        result = self._scan(repos=repos)
        assert result["signal"]["score"] > 0

    def test_empty_results_give_zero_signal(self):
        result = self._scan(token=None, sources=["hn"])
        assert result["signal"]["score"] == 0
        assert result["signal"]["verdict"] == "LOW"

    def test_body_param_influences_queries(self):
        """Additional body text should expand the query list."""
        with patch("priorart.scanner.search_repos", return_value=[]):
            with patch("priorart.scanner.search_discussions", return_value=[]):
                with patch("priorart.scanner.time.sleep"):
                    result_no_body = scan(
                        "Finance app", github_token=None, sources=["hn"]
                    )
                    result_with_body = scan(
                        "Finance app",
                        github_token=None,
                        sources=["hn"],
                        body="Smart budgeting with machine learning insights",
                    )
        assert len(result_with_body["queries"]) >= len(result_no_body["queries"])
