"""Tests for RelaceRepoClient."""

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

import pytest

from relace_mcp.clients.exceptions import RelaceAPIError
from relace_mcp.clients.repo import RelaceRepoClient
from relace_mcp.config import RelaceConfig


@pytest.fixture
def mock_config(tmp_path: Path) -> RelaceConfig:
    return RelaceConfig(
        api_key="rlc-test-api-key-12345",
        base_dir=str(tmp_path),
    )


@pytest.fixture
def repo_client(mock_config: RelaceConfig) -> RelaceRepoClient:
    return RelaceRepoClient(mock_config)


class TestRelaceRepoClientInit:
    """Test RelaceRepoClient initialization."""

    def test_init_with_config(self, mock_config: RelaceConfig) -> None:
        """Should initialize with config."""
        client = RelaceRepoClient(mock_config)
        assert client._config == mock_config
        assert "api.relace.run" in client._base_url


class TestRelaceRepoClientListRepos:
    """Test list_repos method."""

    def test_list_repos_returns_list(self, repo_client: RelaceRepoClient) -> None:
        """Should return list of repos."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.is_success = True
        mock_response.json.return_value = {
            "items": [
                {"repo_id": "repo-1", "metadata": {"name": "test-repo"}},
                {"repo_id": "repo-2", "metadata": {"name": "another-repo"}},
            ]
        }

        with patch.object(repo_client, "_request_with_retry", return_value=mock_response):
            repos = repo_client.list_repos()

        assert len(repos) == 2
        assert repos[0]["metadata"]["name"] == "test-repo"

    def test_list_repos_handles_direct_list_response(self, repo_client: RelaceRepoClient) -> None:
        """Should handle API returning list directly."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.is_success = True
        mock_response.json.return_value = [
            {"id": "repo-1", "name": "test-repo"},
        ]

        with patch.object(repo_client, "_request_with_retry", return_value=mock_response):
            repos = repo_client.list_repos()

        assert len(repos) == 1

    def test_list_repos_paginates_multiple_pages(self, repo_client: RelaceRepoClient) -> None:
        """Should fetch all pages using page_start/next_page cursor pagination."""
        # Page 1: 100 items with next_page cursor
        page1_response = MagicMock()
        page1_response.status_code = 200
        page1_response.is_success = True
        page1_response.json.return_value = {
            "items": [{"repo_id": f"repo-{i}"} for i in range(100)],
            "next_page": 100,  # Cursor to next page
            "total_items": 150,
        }

        # Page 2: 50 items, no next_page = last page
        page2_response = MagicMock()
        page2_response.status_code = 200
        page2_response.is_success = True
        page2_response.json.return_value = {
            "items": [{"repo_id": f"repo-{i}"} for i in range(100, 150)],
            # No next_page = last page
            "total_items": 150,
        }

        with patch.object(
            repo_client, "_request_with_retry", side_effect=[page1_response, page2_response]
        ) as mock_request:
            repos = repo_client.list_repos()

        assert len(repos) == 150
        assert mock_request.call_count == 2
        # Verify page_start parameter usage
        call_args_list = mock_request.call_args_list
        # First call: no page_start (or page_start=0)
        assert call_args_list[0].kwargs["params"].get("page_start", 0) == 0
        # Second call: page_start=100 (from next_page)
        assert call_args_list[1].kwargs["params"]["page_start"] == 100

    def test_list_repos_stops_when_no_next_page(self, repo_client: RelaceRepoClient) -> None:
        """Should stop pagination when next_page is omitted (no more pages)."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.is_success = True
        mock_response.json.return_value = {
            "items": [{"repo_id": "repo-1"}],
            # No next_page = only one page
            "total_items": 1,
        }

        with patch.object(repo_client, "_request_with_retry", return_value=mock_response):
            repos = repo_client.list_repos()

        assert len(repos) == 1

    def test_list_repos_stops_on_empty_page(self, repo_client: RelaceRepoClient) -> None:
        """Should stop pagination when receiving empty response."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.is_success = True
        mock_response.json.return_value = {"items": [], "total_items": 0}

        with patch.object(repo_client, "_request_with_retry", return_value=mock_response):
            repos = repo_client.list_repos()

        assert len(repos) == 0

    def test_list_repos_respects_page_limit(self, repo_client: RelaceRepoClient) -> None:
        """Should stop at 100 pages safety limit to prevent infinite loops."""
        call_count = 0

        def mock_paginated_response(*args, **kwargs):
            nonlocal call_count
            page_start = kwargs.get("params", {}).get("page_start", 0)
            call_count += 1
            mock_resp = MagicMock()
            mock_resp.status_code = 200
            mock_resp.is_success = True
            mock_resp.json.return_value = {
                "items": [{"repo_id": f"repo-{page_start + i}"} for i in range(100)],
                "next_page": page_start + 100,  # Always return next_page
                "total_items": 999999,  # Pretend there are many repos
            }
            return mock_resp

        with patch.object(repo_client, "_request_with_retry", side_effect=mock_paginated_response):
            repos = repo_client.list_repos()

        # Should stop at 100 pages = 10,000 repos
        assert len(repos) == 10000
        assert call_count == 100


class TestRelaceRepoClientCreateRepo:
    """Test create_repo method."""

    def test_create_repo_basic(self, repo_client: RelaceRepoClient) -> None:
        """Should create repo with basic metadata."""
        mock_response = MagicMock()
        mock_response.status_code = 201
        mock_response.is_success = True
        mock_response.json.return_value = {
            "repo_id": "new-repo-id",
            "repo_head": "abc123",
        }

        with patch.object(
            repo_client, "_request_with_retry", return_value=mock_response
        ) as mock_request:
            result = repo_client.create_repo(name="test-repo", auto_index=True)

            call_args = mock_request.call_args
            payload = call_args.kwargs.get("json")
            assert payload is not None
            assert payload["metadata"]["name"] == "test-repo"
            assert payload["auto_index"] is True
            assert "source" not in payload

        assert result["repo_id"] == "new-repo-id"

    def test_create_repo_with_source_files(self, repo_client: RelaceRepoClient) -> None:
        """Should create repo with source files."""
        mock_response = MagicMock()
        mock_response.status_code = 201
        mock_response.is_success = True
        mock_response.json.return_value = {
            "repo_id": "new-repo-id",
            "repo_head": "abc123",
        }

        source = {
            "type": "files",
            "files": [
                {"filename": "src/main.py", "content": "print('hello')"},
            ],
        }

        with patch.object(
            repo_client, "_request_with_retry", return_value=mock_response
        ) as mock_request:
            result = repo_client.create_repo(name="test-repo", auto_index=True, source=source)

            call_args = mock_request.call_args
            payload = call_args.kwargs.get("json")
            assert payload is not None
            assert payload["source"] == source
            assert payload["source"]["type"] == "files"
            assert len(payload["source"]["files"]) == 1

        assert result["repo_id"] == "new-repo-id"

    def test_create_repo_with_source_git(self, repo_client: RelaceRepoClient) -> None:
        """Should create repo from git URL."""
        mock_response = MagicMock()
        mock_response.status_code = 201
        mock_response.is_success = True
        mock_response.json.return_value = {
            "repo_id": "new-repo-id",
            "repo_head": "abc123",
        }

        source = {
            "type": "git",
            "url": "https://github.com/example/repo.git",
            "branch": "main",
        }

        with patch.object(
            repo_client, "_request_with_retry", return_value=mock_response
        ) as mock_request:
            result = repo_client.create_repo(name="cloned-repo", auto_index=False, source=source)

            call_args = mock_request.call_args
            payload = call_args.kwargs.get("json")
            assert payload is not None
            assert payload["source"]["type"] == "git"
            assert payload["source"]["url"] == "https://github.com/example/repo.git"
            assert payload["auto_index"] is False

        assert result["repo_id"] == "new-repo-id"


class TestRelaceRepoClientEnsureRepo:
    """Test ensure_repo method."""

    def test_ensure_repo_uses_cached_id(self, mock_config: RelaceConfig) -> None:
        """Should return cached repo_id if available."""
        with patch("relace_mcp.clients.repo.RELACE_REPO_ID", "cached-repo-id"):
            client = RelaceRepoClient(mock_config)
            repo_id = client.ensure_repo("test-repo")

        assert repo_id == "cached-repo-id"

    def test_ensure_repo_finds_existing(self, repo_client: RelaceRepoClient) -> None:
        """Should find and return existing repo."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.is_success = True
        mock_response.json.return_value = {
            "items": [
                {"repo_id": "existing-repo-id", "metadata": {"name": "test-repo"}},
            ]
        }

        with patch.object(repo_client, "_request_with_retry", return_value=mock_response):
            repo_id = repo_client.ensure_repo("test-repo")

        assert repo_id == "existing-repo-id"

    def test_ensure_repo_creates_new(self, repo_client: RelaceRepoClient) -> None:
        """Should create new repo if not found."""
        list_response = MagicMock()
        list_response.status_code = 200
        list_response.is_success = True
        list_response.json.return_value = {"items": []}

        create_response = MagicMock()
        create_response.status_code = 201
        create_response.is_success = True
        create_response.json.return_value = {"repo_id": "new-repo-id"}

        with patch.object(
            repo_client, "_request_with_retry", side_effect=[list_response, create_response]
        ):
            repo_id = repo_client.ensure_repo("test-repo")

        assert repo_id == "new-repo-id"

    def test_ensure_repo_caches_per_name(self, repo_client: RelaceRepoClient) -> None:
        """Should cache repo IDs per repo name (not globally)."""
        with patch.object(
            repo_client,
            "list_repos",
            side_effect=[
                [{"repo_id": "id-a", "metadata": {"name": "repo-a"}}],
                [{"repo_id": "id-b", "metadata": {"name": "repo-b"}}],
            ],
        ) as mock_list:
            repo_id_a = repo_client.ensure_repo("repo-a")
            repo_id_b = repo_client.ensure_repo("repo-b")
            repo_id_a_again = repo_client.ensure_repo("repo-a")

        assert repo_id_a == "id-a"
        assert repo_id_b == "id-b"
        assert repo_id_a_again == "id-a"
        assert mock_list.call_count == 2


class TestRelaceRepoClientRetrieve:
    """Test retrieve (semantic search) method."""

    def test_retrieve_sends_correct_payload(self, repo_client: RelaceRepoClient) -> None:
        """Should send correct payload for semantic search."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.is_success = True
        mock_response.json.return_value = {
            "results": [
                {"filename": "src/auth.py", "content": "def login(): ..."},
            ]
        }

        with patch.object(
            repo_client, "_request_with_retry", return_value=mock_response
        ) as mock_request:
            result = repo_client.retrieve(
                repo_id="test-repo-id",
                query="user authentication",
                score_threshold=0.5,
                token_limit=10000,
            )

            # Verify payload
            call_args = mock_request.call_args
            payload = call_args.kwargs.get("json")
            assert payload is not None
            assert payload["query"] == "user authentication"
            assert payload["score_threshold"] == 0.5
            assert payload["token_limit"] == 10000
            assert payload["include_content"] is True

        assert len(result["results"]) == 1


class TestRelaceRepoClientRetry:
    """Test retry behavior."""

    def test_retries_on_429(self, repo_client: RelaceRepoClient) -> None:
        """Should retry on rate limit (429) error."""
        call_count = 0

        def mock_request(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                mock_resp = MagicMock()
                mock_resp.status_code = 429
                mock_resp.is_success = False
                mock_resp.text = '{"code": "rate_limit", "message": "Too many requests"}'
                mock_resp.headers = {"retry-after": "0.1"}
                return mock_resp
            else:
                mock_resp = MagicMock()
                mock_resp.status_code = 200
                mock_resp.is_success = True
                mock_resp.json.return_value = {"items": []}
                return mock_resp

        with patch("relace_mcp.clients.repo.httpx.Client") as mock_client_class:
            mock_instance = MagicMock()
            mock_instance.__enter__.return_value = mock_instance
            mock_instance.__exit__.return_value = None
            mock_instance.request = MagicMock(side_effect=mock_request)
            mock_client_class.return_value = mock_instance

            with patch("relace_mcp.clients.repo.time.sleep"):
                repos = repo_client.list_repos()

        assert call_count == 3
        assert repos == []

    def test_raises_on_non_retryable_error(self, repo_client: RelaceRepoClient) -> None:
        """Should raise immediately on non-retryable error (401)."""
        mock_response = MagicMock()
        mock_response.status_code = 401
        mock_response.is_success = False
        mock_response.text = '{"code": "invalid_api_key", "message": "Invalid API key"}'
        mock_response.headers = {}

        with patch("relace_mcp.clients.repo.httpx.Client") as mock_client_class:
            mock_instance = MagicMock()
            mock_instance.__enter__.return_value = mock_instance
            mock_instance.__exit__.return_value = None
            mock_instance.request = MagicMock(return_value=mock_response)
            mock_client_class.return_value = mock_instance

            with pytest.raises(RuntimeError, match="invalid_api_key"):
                repo_client.list_repos()


class TestRelaceRepoClientDeleteRepo:
    """Test delete_repo behavior."""

    def test_delete_repo_treats_404_as_success(self, repo_client: RelaceRepoClient) -> None:
        """Should treat 404 as idempotent success and clear cached repo_id."""
        repo_client._cached_repo_ids["test-repo"] = "test-repo-id"

        api_error = RelaceAPIError(
            status_code=404,
            code="not_found",
            message="Repo not found",
            retryable=False,
        )

        def raise_not_found(*args, **kwargs):
            raise RuntimeError("Repos API error (not_found): Repo not found") from api_error

        with patch.object(repo_client, "_request_with_retry", side_effect=raise_not_found):
            assert repo_client.delete_repo("test-repo-id") is True

        assert "test-repo" not in repo_client._cached_repo_ids

    def test_delete_repo_returns_false_on_other_errors(self, repo_client: RelaceRepoClient) -> None:
        """Should return False for non-404 API errors."""
        repo_client._cached_repo_ids["test-repo"] = "test-repo-id"

        api_error = RelaceAPIError(
            status_code=403,
            code="forbidden",
            message="Forbidden",
            retryable=False,
        )

        def raise_forbidden(*args, **kwargs):
            raise RuntimeError("Repos API error (forbidden): Forbidden") from api_error

        with patch.object(repo_client, "_request_with_retry", side_effect=raise_forbidden):
            assert repo_client.delete_repo("test-repo-id") is False

        # Cached ID should remain unchanged on failure
        assert repo_client._cached_repo_ids["test-repo"] == "test-repo-id"
