"""
Unit tests for orchestration.candidate_validator (Step 09).

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from mcp_proxy_adapter.core.orchestration.candidate_validator import (
    ValidationResult,
    validate_candidate,
    _extract_reported_id,
    _is_valid_uuid4,
)


class TestValidationResult:
    """Tests for ValidationResult dataclass."""

    def test_valid_true_reason_none(self):
        """Valid result has reason None."""
        r = ValidationResult(valid=True, reason=None, reported_id="x")
        assert r.valid is True
        assert r.reason is None

    def test_valid_false_with_reason(self):
        """Invalid result stores reason."""
        r = ValidationResult(valid=False, reason="unreachable")
        assert r.valid is False
        assert r.reason == "unreachable"

    def test_post_init_sets_reason_when_invalid_and_reason_none(self):
        """When valid=False and reason=None, __post_init__ sets reason to invalid_id."""
        r = ValidationResult(valid=False, reason=None)
        assert r.reason == "invalid_id"


class TestIsValidUuid4:
    """Tests for _is_valid_uuid4 helper."""

    def test_valid_uuid4(self):
        """Valid UUID4 string returns True."""
        assert _is_valid_uuid4("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d") is True

    def test_empty_returns_false(self):
        """Empty or whitespace returns False."""
        assert _is_valid_uuid4("") is False
        assert _is_valid_uuid4("   ") is False

    def test_invalid_uuid_version_returns_false(self):
        """Non-UUID4 version returns False (UUID3 example)."""
        # UUID3-style: 3 in 3ca4 is version 3
        assert _is_valid_uuid4("6fa459ea-ee8a-3ca4-894e-db77e160355e") is False

    def test_non_uuid_string_returns_false(self):
        """Non-UUID string returns False."""
        assert _is_valid_uuid4("not-a-uuid") is False
        assert _is_valid_uuid4("123") is False

    def test_none_returns_false(self):
        """None returns False."""
        assert _is_valid_uuid4(None) is False  # type: ignore[arg-type]


class TestExtractReportedId:
    """Tests for _extract_reported_id."""

    def test_top_level_instance_uuid(self):
        """Top-level instance_uuid is returned."""
        body = {"instance_uuid": "  a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d  "}
        assert _extract_reported_id(body) == "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"

    def test_top_level_uuid(self):
        """Top-level uuid is used when instance_uuid absent."""
        body = {"status": "ok", "uuid": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"}
        assert _extract_reported_id(body) == "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"

    def test_components_proxy_registration_instance_uuid(self):
        """components.proxy_registration.instance_uuid is used when top-level absent."""
        body = {
            "status": "ok",
            "components": {
                "proxy_registration": {
                    "instance_uuid": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"
                },
            },
        }
        assert _extract_reported_id(body) == "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"

    def test_empty_or_missing_returns_none(self):
        """Missing or empty identity returns None."""
        assert _extract_reported_id({}) is None
        assert _extract_reported_id({"instance_uuid": ""}) is None
        assert _extract_reported_id({"instance_uuid": "   "}) is None


@pytest.mark.asyncio
class TestValidateCandidate:
    """Tests for validate_candidate async function."""

    async def test_unreachable_returns_unreachable_reason(self):
        """When GET raises, result is valid=False, reason=unreachable."""
        with patch(
            "mcp_proxy_adapter.core.orchestration.candidate_validator.httpx.AsyncClient"
        ) as mock_client_class:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client.get = AsyncMock(side_effect=ConnectionError("refused"))
            mock_client_class.return_value = mock_client

            result = await validate_candidate(
                "127.0.0.1", 9999, "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
            )
            assert result.valid is False
            assert result.reason == "unreachable"

    async def test_invalid_json_returns_invalid_id(self):
        """When response is not JSON, result is invalid_id."""
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_response.json = MagicMock(side_effect=ValueError("not json"))

        with patch(
            "mcp_proxy_adapter.core.orchestration.candidate_validator.httpx.AsyncClient"
        ) as mock_client_class:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client.get = AsyncMock(return_value=mock_response)
            mock_client_class.return_value = mock_client

            result = await validate_candidate(
                "host", 8080, "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
            )
            assert result.valid is False
            assert result.reason == "invalid_id"

    async def test_no_identity_in_body_returns_invalid_id(self):
        """When body has no identity field, result is invalid_id."""
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_response.json = MagicMock(return_value={"status": "ok"})

        with patch(
            "mcp_proxy_adapter.core.orchestration.candidate_validator.httpx.AsyncClient"
        ) as mock_client_class:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client.get = AsyncMock(return_value=mock_response)
            mock_client_class.return_value = mock_client

            result = await validate_candidate(
                "host", 8080, "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
            )
            assert result.valid is False
            assert result.reason == "invalid_id"

    async def test_reported_id_not_uuid4_returns_invalid_id(self):
        """When reported id is not UUID4, result is invalid_id."""
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_response.json = MagicMock(return_value={"instance_uuid": "not-a-uuid"})

        with patch(
            "mcp_proxy_adapter.core.orchestration.candidate_validator.httpx.AsyncClient"
        ) as mock_client_class:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client.get = AsyncMock(return_value=mock_response)
            mock_client_class.return_value = mock_client

            result = await validate_candidate(
                "host", 8080, "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
            )
            assert result.valid is False
            assert result.reason == "invalid_id"
            assert result.reported_id == "not-a-uuid"

    async def test_id_mismatch_returns_id_mismatch(self):
        """When reported id differs from expected, result is id_mismatch."""
        expected = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
        reported = "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_response.json = MagicMock(return_value={"instance_uuid": reported})

        with patch(
            "mcp_proxy_adapter.core.orchestration.candidate_validator.httpx.AsyncClient"
        ) as mock_client_class:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client.get = AsyncMock(return_value=mock_response)
            mock_client_class.return_value = mock_client

            result = await validate_candidate("host", 8080, expected)
            assert result.valid is False
            assert result.reason == "id_mismatch"
            assert result.reported_id == reported

    async def test_success_when_id_matches(self):
        """When reachable and reported UUID4 matches expected, result is valid."""
        uid = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_response.json = MagicMock(return_value={"instance_uuid": uid})

        with patch(
            "mcp_proxy_adapter.core.orchestration.candidate_validator.httpx.AsyncClient"
        ) as mock_client_class:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client.get = AsyncMock(return_value=mock_response)
            mock_client_class.return_value = mock_client

            result = await validate_candidate("host", 8080, uid)
            assert result.valid is True
            assert result.reason is None
            assert result.reported_id == uid

    async def test_success_case_insensitive_match(self):
        """ID comparison is case-insensitive."""
        uid_lower = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
        uid_upper = "A1B2C3D4-E5F6-4A7B-8C9D-0E1F2A3B4C5D"
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_response.json = MagicMock(return_value={"uuid": uid_upper})

        with patch(
            "mcp_proxy_adapter.core.orchestration.candidate_validator.httpx.AsyncClient"
        ) as mock_client_class:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client.get = AsyncMock(return_value=mock_response)
            mock_client_class.return_value = mock_client

            result = await validate_candidate("host", 8080, uid_lower)
            assert result.valid is True
            assert result.reported_id == uid_upper

    async def test_get_called_with_health_url(self):
        """validate_candidate calls GET with correct health URL."""
        uid = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_response.json = MagicMock(return_value={"instance_uuid": uid})

        with patch(
            "mcp_proxy_adapter.core.orchestration.candidate_validator.httpx.AsyncClient"
        ) as mock_client_class:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client.get = AsyncMock(return_value=mock_response)
            mock_client_class.return_value = mock_client

            await validate_candidate("example.com", 8443, uid, protocol="https")
            mock_client.get.assert_called_once()
            call_url = mock_client.get.call_args[0][0]
            assert call_url == "https://example.com:8443/health"
