"""Unit tests for CLI api-check command."""

from __future__ import annotations

import json
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from tests.conftest import CliRunner

from teams_phone.cli.main import app
from teams_phone.constants import ExitCode
from teams_phone.exceptions import AuthenticationError

runner = CliRunner()


class TestApiCheckCommand:
    """Tests for api-check command."""

    @pytest.mark.unit
    def test_api_check_shows_in_help(self) -> None:
        """api-check command appears in main help."""
        result = runner.invoke(app, ["--help"])
        assert result.exit_code == 0
        assert "api-check" in result.stdout

    @pytest.mark.unit
    def test_api_check_has_own_help(self) -> None:
        """api-check command has its own help text."""
        result = runner.invoke(app, ["api-check", "--help"])
        assert result.exit_code == 0
        assert "Validate API contracts" in result.plain_stdout
        assert "--endpoint" in result.plain_stdout
        assert "--verbose" in result.plain_stdout

    @pytest.mark.unit
    def test_api_check_pass_all_endpoints(self) -> None:
        """api-check passes when all endpoints validate successfully."""
        # Valid API response data that matches our Pydantic models
        # Note: enum values use camelCase as returned by Graph API
        user_config_response = {
            "@odata.context": "https://graph.microsoft.com/beta/$metadata",
            "value": [
                {
                    "id": "user-123",
                    "userPrincipalName": "user@example.com",
                    "tenantId": "tenant-456",
                    "displayName": "Test User",
                    "accountType": "user",
                    "isEnterpriseVoiceEnabled": True,
                    "featureTypes": ["Teams"],
                    "telephoneNumbers": [],
                    "effectivePolicyAssignments": [],
                }
            ],
        }
        number_assignment_response = {
            "@odata.context": "https://graph.microsoft.com/beta/$metadata",
            "value": [
                {
                    "id": "num-789",
                    "telephoneNumber": "+14155551234",
                    "numberType": "callingPlan",
                    "activationState": "activated",
                    "capabilities": ["userAssignment"],
                    "assignmentStatus": "userAssigned",
                }
            ],
        }

        async def mock_get(
            endpoint: str, *, params: dict[str, Any] | None = None
        ) -> dict[str, Any]:
            if "userConfigurations" in endpoint:
                return user_config_response
            elif "numberAssignments" in endpoint:
                return number_assignment_response
            return {"value": []}

        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService"),
            patch("teams_phone.cli.api_check.GraphClient") as mock_graph_class,
        ):
            mock_graph = AsyncMock()
            mock_graph.get = mock_get
            mock_graph.__aenter__.return_value = mock_graph
            mock_graph.__aexit__.return_value = None
            mock_graph_class.return_value = mock_graph

            result = runner.invoke(app, ["--no-color", "api-check"])
            assert result.exit_code == 0
            assert "PASS" in result.stdout
            assert "validated successfully" in result.stdout

    @pytest.mark.unit
    def test_api_check_fail_extra_field(self) -> None:
        """api-check fails when response has unexpected extra fields."""
        # Response with extra field that Pydantic strict mode will reject
        response_with_extra_field = {
            "value": [
                {
                    "id": "user-123",
                    "userPrincipalName": "user@example.com",
                    "tenantId": "tenant-456",
                    "accountType": "User",
                    "isEnterpriseVoiceEnabled": True,
                    "unknownNewField": "this should fail validation",  # Extra field!
                }
            ],
        }

        async def mock_get(
            endpoint: str, *, params: dict[str, Any] | None = None
        ) -> dict[str, Any]:
            return response_with_extra_field

        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService"),
            patch("teams_phone.cli.api_check.GraphClient") as mock_graph_class,
        ):
            mock_graph = AsyncMock()
            mock_graph.get = mock_get
            mock_graph.__aenter__.return_value = mock_graph
            mock_graph.__aexit__.return_value = None
            mock_graph_class.return_value = mock_graph

            result = runner.invoke(app, ["--no-color", "api-check"])
            assert result.exit_code == ExitCode.CONTRACT_ERROR
            assert "FAIL" in result.stdout

    @pytest.mark.unit
    def test_api_check_fail_missing_required_field(self) -> None:
        """api-check fails when required field is missing."""
        # Response missing required 'telephoneNumber' field
        response_missing_field = {
            "value": [
                {
                    "id": "num-789",
                    # "telephoneNumber" is required but missing
                    "numberType": "CallingPlan",
                    "activationState": "Activated",
                    "capabilities": ["userAssignment"],
                    "assignmentStatus": "Assigned",
                }
            ],
        }

        async def mock_get(
            endpoint: str, *, params: dict[str, Any] | None = None
        ) -> dict[str, Any]:
            if "numberAssignments" in endpoint:
                return response_missing_field
            return {"value": []}

        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService"),
            patch("teams_phone.cli.api_check.GraphClient") as mock_graph_class,
        ):
            mock_graph = AsyncMock()
            mock_graph.get = mock_get
            mock_graph.__aenter__.return_value = mock_graph
            mock_graph.__aexit__.return_value = None
            mock_graph_class.return_value = mock_graph

            result = runner.invoke(
                app, ["--no-color", "api-check", "--endpoint", "numberAssignments"]
            )
            assert result.exit_code == ExitCode.CONTRACT_ERROR
            assert "FAIL" in result.stdout

    @pytest.mark.unit
    def test_api_check_specific_endpoint(self) -> None:
        """api-check can validate a specific endpoint only."""
        user_config_response = {
            "value": [
                {
                    "id": "user-123",
                    "userPrincipalName": "user@example.com",
                    "tenantId": "tenant-456",
                    "displayName": "Test User",
                    "accountType": "user",
                    "isEnterpriseVoiceEnabled": True,
                    "featureTypes": [],
                    "telephoneNumbers": [],
                    "effectivePolicyAssignments": [],
                }
            ],
        }

        async def mock_get(
            endpoint: str, *, params: dict[str, Any] | None = None
        ) -> dict[str, Any]:
            # Should only be called for userConfigurations
            assert "userConfigurations" in endpoint
            return user_config_response

        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService"),
            patch("teams_phone.cli.api_check.GraphClient") as mock_graph_class,
        ):
            mock_graph = AsyncMock()
            mock_graph.get = mock_get
            mock_graph.__aenter__.return_value = mock_graph
            mock_graph.__aexit__.return_value = None
            mock_graph_class.return_value = mock_graph

            result = runner.invoke(
                app, ["--no-color", "api-check", "--endpoint", "userConfigurations"]
            )
            assert result.exit_code == 0
            assert "userConfigurations" in result.stdout
            assert "numberAssignments" not in result.stdout

    @pytest.mark.unit
    def test_api_check_invalid_endpoint(self) -> None:
        """api-check rejects invalid endpoint names."""
        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService"),
        ):
            result = runner.invoke(
                app, ["--no-color", "api-check", "--endpoint", "invalidEndpoint"]
            )
            assert result.exit_code == ExitCode.VALIDATION_ERROR
            assert "Unknown endpoint" in result.stdout

    @pytest.mark.unit
    def test_api_check_verbose_shows_details(self) -> None:
        """api-check --verbose shows detailed error information."""
        response_with_extra_field = {
            "value": [
                {
                    "id": "user-123",
                    "userPrincipalName": "user@example.com",
                    "tenantId": "tenant-456",
                    "accountType": "User",
                    "isEnterpriseVoiceEnabled": True,
                    "unexpectedField": "causes failure",
                }
            ],
        }

        async def mock_get(
            endpoint: str, *, params: dict[str, Any] | None = None
        ) -> dict[str, Any]:
            return response_with_extra_field

        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService"),
            patch("teams_phone.cli.api_check.GraphClient") as mock_graph_class,
        ):
            mock_graph = AsyncMock()
            mock_graph.get = mock_get
            mock_graph.__aenter__.return_value = mock_graph
            mock_graph.__aexit__.return_value = None
            mock_graph_class.return_value = mock_graph

            result = runner.invoke(app, ["--no-color", "api-check", "--verbose"])
            assert result.exit_code == ExitCode.CONTRACT_ERROR
            # Verbose mode should show field details
            assert "unexpectedField" in result.stdout

    @pytest.mark.unit
    def test_api_check_json_output(self) -> None:
        """api-check --json outputs structured JSON."""
        user_config_response = {
            "value": [
                {
                    "id": "user-123",
                    "userPrincipalName": "user@example.com",
                    "tenantId": "tenant-456",
                    "displayName": "Test User",
                    "accountType": "user",
                    "isEnterpriseVoiceEnabled": True,
                    "featureTypes": [],
                    "telephoneNumbers": [],
                    "effectivePolicyAssignments": [],
                }
            ],
        }
        number_assignment_response = {
            "value": [
                {
                    "id": "num-789",
                    "telephoneNumber": "+14155551234",
                    "numberType": "callingPlan",
                    "activationState": "activated",
                    "capabilities": ["userAssignment"],
                    "assignmentStatus": "userAssigned",
                }
            ],
        }

        async def mock_get(
            endpoint: str, *, params: dict[str, Any] | None = None
        ) -> dict[str, Any]:
            if "userConfigurations" in endpoint:
                return user_config_response
            elif "numberAssignments" in endpoint:
                return number_assignment_response
            return {"value": []}

        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService"),
            patch("teams_phone.cli.api_check.GraphClient") as mock_graph_class,
        ):
            mock_graph = AsyncMock()
            mock_graph.get = mock_get
            mock_graph.__aenter__.return_value = mock_graph
            mock_graph.__aexit__.return_value = None
            mock_graph_class.return_value = mock_graph

            result = runner.invoke(app, ["--json", "api-check"])
            assert result.exit_code == 0

            # Parse JSON output
            output = json.loads(result.stdout)
            assert output["status"] == "pass"
            assert len(output["results"]) == 2
            assert all(r["status"] == "PASS" for r in output["results"])

    @pytest.mark.unit
    def test_api_check_empty_endpoint_data(self) -> None:
        """api-check handles endpoints with no data gracefully."""
        empty_response: dict[str, Any] = {"value": []}

        async def mock_get(
            endpoint: str, *, params: dict[str, Any] | None = None
        ) -> dict[str, Any]:
            return empty_response

        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService"),
            patch("teams_phone.cli.api_check.GraphClient") as mock_graph_class,
        ):
            mock_graph = AsyncMock()
            mock_graph.get = mock_get
            mock_graph.__aenter__.return_value = mock_graph
            mock_graph.__aexit__.return_value = None
            mock_graph_class.return_value = mock_graph

            result = runner.invoke(app, ["--no-color", "api-check"])
            # Empty data should still pass (nothing to validate)
            assert result.exit_code == 0
            assert "PASS" in result.stdout

    @pytest.mark.unit
    def test_api_check_auth_error(self) -> None:
        """api-check handles authentication errors appropriately."""
        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService") as mock_auth_class,
        ):
            mock_auth = MagicMock()
            mock_auth_class.return_value = mock_auth

            # Simulate auth error when GraphClient tries to authenticate
            with patch("teams_phone.cli.api_check.GraphClient") as mock_graph_class:
                mock_graph = AsyncMock()
                mock_graph.__aenter__.side_effect = AuthenticationError("Token expired")
                mock_graph_class.return_value = mock_graph

                result = runner.invoke(app, ["--no-color", "api-check"])
                assert result.exit_code == ExitCode.AUTH_ERROR
                assert "Token expired" in result.stdout

    @pytest.mark.unit
    def test_api_check_strips_odata_metadata(self) -> None:
        """api-check properly strips @odata fields before validation."""
        # Response with OData metadata that should be stripped
        response_with_odata = {
            "@odata.context": "https://graph.microsoft.com/beta/$metadata#...",
            "@odata.count": 1,
            "value": [
                {
                    "@odata.type": "#microsoft.graph.userConfiguration",
                    "id": "user-123",
                    "userPrincipalName": "user@example.com",
                    "tenantId": "tenant-456",
                    "displayName": "Test User",
                    "accountType": "user",
                    "isEnterpriseVoiceEnabled": True,
                    "featureTypes": [],
                    "telephoneNumbers": [],
                    "effectivePolicyAssignments": [],
                }
            ],
        }

        async def mock_get(
            endpoint: str, *, params: dict[str, Any] | None = None
        ) -> dict[str, Any]:
            return response_with_odata

        with (
            patch("teams_phone.cli.api_check.CacheManager"),
            patch("teams_phone.cli.api_check.AuthService"),
            patch("teams_phone.cli.api_check.GraphClient") as mock_graph_class,
        ):
            mock_graph = AsyncMock()
            mock_graph.get = mock_get
            mock_graph.__aenter__.return_value = mock_graph
            mock_graph.__aexit__.return_value = None
            mock_graph_class.return_value = mock_graph

            result = runner.invoke(
                app, ["--no-color", "api-check", "--endpoint", "userConfigurations"]
            )
            # Should pass because @odata.type in item should be stripped
            assert result.exit_code == 0
            assert "PASS" in result.stdout
