from __future__ import annotations

import json
from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest

from mcp_server_propel.propel_client import (
    MAX_DIFF_BYTES,
    PropelAPIError,
    PropelAuthError,
    PropelClient,
    PropelDiffTooLargeError,
    PropelNotFoundError,
    PropelValidationError,
)
from mcp_server_propel.tools.review_tools import (
    check_review_status,
    get_review,
    submit_review,
)
from tests.conftest import (
    SAMPLE_COMPLETED_NO_COMMENTS_RESPONSE,
    SAMPLE_COMPLETED_RESPONSE,
    SAMPLE_FAILED_RESPONSE,
    SAMPLE_QUEUED_RESPONSE,
    SAMPLE_RUNNING_RESPONSE,
    make_mock_transport,
    make_propel_client,
)


def _make_ctx(client: PropelClient) -> MagicMock:
    ctx = MagicMock()
    ctx.request_context.lifespan_context = client
    ctx.info = AsyncMock()
    ctx.warning = AsyncMock()
    ctx.report_progress = AsyncMock()
    return ctx


class TestPropelClientCreateReview:
    @pytest.mark.asyncio
    async def test_success(self):
        def handler(request: httpx.Request) -> httpx.Response:
            body = json.loads(request.content)
            assert body["diff"] == "diff content"
            assert body["repository"] == "myorg/myrepo"
            return httpx.Response(202, json=SAMPLE_QUEUED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        result = await client.create_review("diff content", "myorg/myrepo")
        assert result["review_id"] == "abc-123"
        assert result["status"] == "queued"
        await client.aclose()

    @pytest.mark.asyncio
    async def test_diff_too_large(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        large_diff = "x" * (MAX_DIFF_BYTES + 1)
        with pytest.raises(PropelDiffTooLargeError):
            await client.create_review(large_diff, "myorg/myrepo")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_diff_too_large_unicode(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        emoji_count = (MAX_DIFF_BYTES // 4) + 1
        large_diff = "\U0001f600" * emoji_count
        assert len(large_diff) < MAX_DIFF_BYTES  # fewer chars than limit
        assert len(large_diff.encode("utf-8")) > MAX_DIFF_BYTES  # more bytes than limit
        with pytest.raises(PropelDiffTooLargeError):
            await client.create_review(large_diff, "myorg/myrepo")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_repository_too_long(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        with pytest.raises(PropelValidationError, match="Repository name too long"):
            await client.create_review("diff", "x" * 256)
        await client.aclose()

    @pytest.mark.asyncio
    async def test_auth_error_401(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(401, json={"error": "Invalid authorization token"})

        client = await make_propel_client(make_mock_transport(handler))
        with pytest.raises(PropelAuthError, match="Invalid authorization token"):
            await client.create_review("diff", "myorg/myrepo")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_auth_error_403_expired(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(
                403, json={"error": "Authorization token has expired"}
            )

        client = await make_propel_client(make_mock_transport(handler))
        with pytest.raises(PropelAuthError, match="expired"):
            await client.create_review("diff", "myorg/myrepo")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_not_found_404(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(404, json={"error": "Repository not found"})

        client = await make_propel_client(make_mock_transport(handler))
        with pytest.raises(PropelNotFoundError):
            await client.create_review("diff", "myorg/myrepo")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_server_error_retries(self, monkeypatch):
        call_count = 0

        def handler(request: httpx.Request) -> httpx.Response:
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                return httpx.Response(500, json={"error": "Internal Error"})
            return httpx.Response(202, json=SAMPLE_QUEUED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        import mcp_server_propel.propel_client as client_module

        monkeypatch.setattr(client_module.asyncio, "sleep", AsyncMock())
        result = await client.create_review("diff", "myorg/myrepo")
        assert result["status"] == "queued"
        assert call_count == 3
        await client.aclose()

    @pytest.mark.asyncio
    async def test_server_error_exhausts_retries(self, monkeypatch):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(502, json={"error": "Bad Gateway"})

        client = await make_propel_client(make_mock_transport(handler))
        import mcp_server_propel.propel_client as client_module

        monkeypatch.setattr(client_module.asyncio, "sleep", AsyncMock())
        with pytest.raises(PropelAPIError, match="please try again later"):
            await client.create_review("diff", "myorg/myrepo")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_400_passes_through_server_message(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(400, json={"error": "invalid diff format"})

        client = await make_propel_client(make_mock_transport(handler))
        with pytest.raises(PropelValidationError, match="invalid diff format"):
            await client.create_review("some bad diff", "myorg/myrepo")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_empty_diff_rejected(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        with pytest.raises(PropelValidationError, match="diff is required"):
            await client.create_review("", "myorg/myrepo")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_whitespace_only_diff_rejected(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        with pytest.raises(PropelValidationError, match="diff is required"):
            await client.create_review("   \n\t  ", "myorg/myrepo")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_empty_repository_rejected(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        with pytest.raises(PropelValidationError, match="repository is required"):
            await client.create_review("diff content", "")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_whitespace_only_repository_rejected(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        with pytest.raises(PropelValidationError, match="repository is required"):
            await client.create_review("diff content", "   ")
        await client.aclose()

    @pytest.mark.asyncio
    async def test_inputs_are_trimmed(self):
        def handler(request: httpx.Request) -> httpx.Response:
            body = json.loads(request.content)
            assert body["diff"] == "diff content"
            assert body["repository"] == "myorg/myrepo"
            assert body["base_commit"] == "abc123"
            return httpx.Response(202, json=SAMPLE_QUEUED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        result = await client.create_review(
            "  diff content  ", "  myorg/myrepo  ", base_commit="  abc123  "
        )
        assert result["status"] == "queued"
        await client.aclose()

    @pytest.mark.asyncio
    async def test_403_passes_through_scope_message(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(
                403, json={"error": "Authorization token lacks required scope"}
            )

        client = await make_propel_client(make_mock_transport(handler))
        with pytest.raises(PropelAuthError, match="lacks required scope"):
            await client.create_review("diff", "myorg/myrepo")
        await client.aclose()


class TestPropelClientGetReview:
    @pytest.mark.asyncio
    async def test_success(self):
        def handler(request: httpx.Request) -> httpx.Response:
            assert "/v1/reviews/abc-123" in str(request.url)
            return httpx.Response(200, json=SAMPLE_COMPLETED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        result = await client.get_review("abc-123")
        assert result["status"] == "completed"
        assert len(result["comments"]) == 2
        await client.aclose()


class TestSubmitReviewTool:
    @pytest.mark.asyncio
    async def test_success(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(202, json=SAMPLE_QUEUED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await submit_review("myorg/myrepo", diff="diff content", ctx=ctx)
        assert "Review submitted successfully!" in result
        assert "abc-123" in result
        assert "get_review" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_diff_too_large_returns_error_text(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        ctx = _make_ctx(client)
        large_diff = "x" * (MAX_DIFF_BYTES + 1)
        result = await submit_review("myorg/myrepo", diff=large_diff, ctx=ctx)
        assert "Error" in result
        assert "too large" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_api_error_returns_error_text(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(401, json={"error": "Invalid authorization token"})

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await submit_review("myorg/myrepo", diff="diff", ctx=ctx)
        assert "Error" in result
        assert "Invalid authorization token" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_diff_path_reads_file(self, tmp_path):
        diff_file = tmp_path / "test.diff"
        diff_file.write_text("diff --git a/f.py b/f.py\n--- a/f.py\n+++ b/f.py\n")

        def handler(request: httpx.Request) -> httpx.Response:
            body = json.loads(request.content)
            assert "diff --git" in body["diff"]
            return httpx.Response(202, json=SAMPLE_QUEUED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await submit_review("myorg/myrepo", diff_path=str(diff_file), ctx=ctx)
        assert "Review submitted successfully!" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_diff_path_not_found(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        ctx = _make_ctx(client)
        result = await submit_review(
            "myorg/myrepo", diff_path="/tmp/nonexistent_propel_diff.txt", ctx=ctx
        )
        assert "Error" in result
        assert "not a file" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_diff_path_overrides_diff_string(self, tmp_path):
        diff_file = tmp_path / "test.diff"
        diff_file.write_text("file diff content")

        def handler(request: httpx.Request) -> httpx.Response:
            body = json.loads(request.content)
            assert body["diff"] == "file diff content"
            return httpx.Response(202, json=SAMPLE_QUEUED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await submit_review(
            "myorg/myrepo", diff="ignored", diff_path=str(diff_file), ctx=ctx
        )
        assert "Review submitted successfully!" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_diff_path_too_large(self, tmp_path):
        diff_file = tmp_path / "large.diff"
        diff_file.write_bytes(b"x" * (MAX_DIFF_BYTES + 1))

        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        ctx = _make_ctx(client)
        result = await submit_review("myorg/myrepo", diff_path=str(diff_file), ctx=ctx)
        assert "Error" in result
        assert "File too large" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_diff_path_binary_file(self, tmp_path):
        binary_file = tmp_path / "image.bin"
        binary_file.write_bytes(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR")

        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        ctx = _make_ctx(client)
        result = await submit_review(
            "myorg/myrepo", diff_path=str(binary_file), ctx=ctx
        )
        assert "Error" in result
        assert "not a text file" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_no_diff_and_no_diff_path_returns_error(self):
        client = await make_propel_client(
            make_mock_transport(lambda r: httpx.Response(200))
        )
        ctx = _make_ctx(client)
        result = await submit_review("myorg/myrepo", ctx=ctx)
        assert "Error" in result
        assert "diff is required" in result
        await client.aclose()


class TestGetReviewTool:
    @pytest.mark.asyncio
    async def test_completed_with_comments(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(200, json=SAMPLE_COMPLETED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await get_review("abc-123", ctx=ctx)
        assert "Review completed" in result
        assert "src/app.py:42" in result
        assert "[recommended]" in result
        assert "src/utils.py:15" in result
        assert "[important]" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_completed_no_comments(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(200, json=SAMPLE_COMPLETED_NO_COMMENTS_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await get_review("abc-123", ctx=ctx)
        assert "No issues found" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_failed(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(200, json=SAMPLE_FAILED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await get_review("abc-123", ctx=ctx)
        assert "Review failed" in result
        assert "generation_failed" in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_polls_until_complete(self, monkeypatch):
        call_count = 0

        def handler(request: httpx.Request) -> httpx.Response:
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                return httpx.Response(200, json=SAMPLE_RUNNING_RESPONSE)
            return httpx.Response(200, json=SAMPLE_COMPLETED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)

        import mcp_server_propel.tools.review_tools as tools_module

        monkeypatch.setattr(tools_module.asyncio, "sleep", AsyncMock())

        result = await get_review("abc-123", ctx=ctx)
        assert "Review completed" in result
        assert call_count == 3
        assert ctx.report_progress.call_count == 3
        await client.aclose()

    @pytest.mark.asyncio
    async def test_api_error_returns_error_text(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(404, json={"error": "Not found"})

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await get_review("abc-123", ctx=ctx)
        assert "Error" in result
        await client.aclose()


class TestCheckReviewStatusTool:
    @pytest.mark.asyncio
    async def test_running(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(200, json=SAMPLE_RUNNING_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await check_review_status("abc-123", ctx=ctx)
        assert "running" in result
        assert "abc-123" in result
        assert "src/app.py" not in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_completed_shows_count_not_details(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(200, json=SAMPLE_COMPLETED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await check_review_status("abc-123", ctx=ctx)
        assert "completed" in result
        assert "Comments: 2" in result
        assert "list comprehension" not in result
        await client.aclose()

    @pytest.mark.asyncio
    async def test_failed_shows_error(self):
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(200, json=SAMPLE_FAILED_RESPONSE)

        client = await make_propel_client(make_mock_transport(handler))
        ctx = _make_ctx(client)
        result = await check_review_status("abc-123", ctx=ctx)
        assert "failed" in result
        assert "generation_failed" in result or "internal error" in result.lower()
        await client.aclose()


class TestClientTimeout:
    def test_timeout_is_30_seconds(self):
        client = PropelClient(api_token="test", base_url="https://api.test")
        timeout = client._client.timeout
        assert timeout.connect == 30.0
        assert timeout.read == 30.0
        assert timeout.write == 30.0
