"""Integration tests for CLI policies commands.

This module tests policies command workflows including list/show/assign
operations, filter options, staleness warnings, and output format validation
(table/JSON).
"""

from __future__ import annotations

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

import pytest
from typer.testing import CliRunner

from teams_phone.cli.main import app
from teams_phone.exceptions import NotFoundError

from tests.integration.cli.conftest import (
    make_policy,
    make_policy_assignment,
    make_user_config,
)


class TestPoliciesListIntegration:
    """Integration tests for policies list command."""

    @pytest.mark.integration
    def test_list_all_policies_table_output(
        self,
        runner: CliRunner,
        mock_cli_services_policies: dict[str, MagicMock],
    ) -> None:
        """List without filter returns all policies in table format."""
        mocks = mock_cli_services_policies
        policies = [
            make_policy(
                policy_type="TeamsCallingPolicy",
                policy_name="AllowCalling",
                description="Allow calling",
            ),
            make_policy(
                policy_type="TenantDialPlan",
                policy_name="US-DialPlan",
                policy_id="policy-22222222-2222-2222-2222-222222222222",
                description="US dial plan",
            ),
        ]
        mocks["policy_service"].list_policies.return_value = policies
        mocks["policy_service"].get_staleness_warning.return_value = None

        result = runner.invoke(app, ["--no-color", "policies", "list"])
        assert result.exit_code == 0
        assert "AllowCalling" in result.stdout
        assert "US-DialPlan" in result.stdout
        assert "calling" in result.stdout  # Short policy type display

    @pytest.mark.integration
    def test_list_policies_json_output(
        self,
        runner: CliRunner,
        mock_cli_services_policies: dict[str, MagicMock],
    ) -> None:
        """List policies with --json produces valid JSON array."""
        mocks = mock_cli_services_policies
        policies = [
            make_policy(
                policy_type="TeamsCallingPolicy",
                policy_name="AllowCalling",
                policy_id="policy-12345",
                description="Allow calling",
                is_global=True,
            ),
        ]
        mocks["policy_service"].list_policies.return_value = policies
        mocks["policy_service"].get_staleness_warning.return_value = None

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

        data = json.loads(result.stdout)
        assert isinstance(data, list)
        assert len(data) == 1
        assert data[0]["policy_type"] == "TeamsCallingPolicy"
        assert data[0]["policy_name"] == "AllowCalling"
        assert data[0]["policy_id"] == "policy-12345"
        assert data[0]["is_global"] is True

    @pytest.mark.integration
    def test_list_policies_type_filter(
        self,
        runner: CliRunner,
        mock_cli_services_policies: dict[str, MagicMock],
    ) -> None:
        """Policy type argument filters results."""
        mocks = mock_cli_services_policies
        policies = [
            make_policy(
                policy_type="TeamsCallingPolicy",
                policy_name="AllowCalling",
            ),
        ]
        mocks["policy_service"].list_policies.return_value = policies
        mocks["policy_service"].get_staleness_warning.return_value = None

        result = runner.invoke(app, ["--no-color", "policies", "list", "calling"])
        assert result.exit_code == 0
        assert "AllowCalling" in result.stdout

        # Verify the service was called with resolved policy type
        call_kwargs = mocks["policy_service"].list_policies.call_args[1]
        assert call_kwargs["policy_type"] == "TeamsCallingPolicy"

    @pytest.mark.integration
    def test_list_policies_staleness_warning(
        self,
        runner: CliRunner,
        mock_cli_services_policies: dict[str, MagicMock],
    ) -> None:
        """Staleness warning is displayed when cache is stale."""
        mocks = mock_cli_services_policies
        policies = [make_policy(policy_name="AllowCalling")]
        mocks["policy_service"].list_policies.return_value = policies
        mocks[
            "policy_service"
        ].get_staleness_warning.return_value = (
            "Policy cache is stale. Run 'Export-Policies.ps1' to refresh."
        )

        result = runner.invoke(app, ["--no-color", "policies", "list"])
        assert result.exit_code == 0
        assert "stale" in result.stdout.lower()
        assert "AllowCalling" in result.stdout

    @pytest.mark.integration
    def test_list_policies_empty_result(
        self,
        runner: CliRunner,
        mock_cli_services_policies: dict[str, MagicMock],
    ) -> None:
        """Empty result shows info message in table mode."""
        mocks = mock_cli_services_policies
        mocks["policy_service"].list_policies.return_value = []
        mocks["policy_service"].get_staleness_warning.return_value = None

        result = runner.invoke(app, ["--no-color", "policies", "list"])
        assert result.exit_code == 0
        assert "No policies found" in result.stdout


class TestPoliciesShowIntegration:
    """Integration tests for policies show command."""

    @pytest.fixture
    def mock_cli_services_policies_show(
        self,
        mock_user_service: MagicMock,
        mock_cache_manager: MagicMock,
        mock_auth_service: MagicMock,
    ) -> Generator[dict[str, MagicMock], None, None]:
        """Patch all services needed for policies show command."""
        mock_gc = AsyncMock()
        mock_gc.__aenter__.return_value = mock_gc
        mock_gc.__aexit__.return_value = None

        # PolicyService needs get_user_policies as AsyncMock
        mock_policy_service = MagicMock()
        mock_policy_service.get_user_policies = AsyncMock(return_value=[])

        with (
            patch(
                "teams_phone.cli.policies.CacheManager", return_value=mock_cache_manager
            ),
            patch(
                "teams_phone.cli.policies.AuthService", return_value=mock_auth_service
            ),
            patch("teams_phone.cli.policies.GraphClient", return_value=mock_gc),
            patch(
                "teams_phone.cli.policies.PolicyService",
                return_value=mock_policy_service,
            ),
            patch(
                "teams_phone.cli.policies.UserService", return_value=mock_user_service
            ),
        ):
            yield {
                "cache_manager": mock_cache_manager,
                "auth_service": mock_auth_service,
                "graph_client": mock_gc,
                "policy_service": mock_policy_service,
                "user_service": mock_user_service,
            }

    @pytest.mark.integration
    def test_show_user_policies_table_output(
        self,
        runner: CliRunner,
        mock_cli_services_policies_show: dict[str, MagicMock],
    ) -> None:
        """Show user policies displays policy assignments."""
        mocks = mock_cli_services_policies_show
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        assignments = [
            make_policy_assignment(
                policy_type="TeamsCallingPolicy",
                display_name="AllowCalling",
                assignment_type="direct",
            ),
            make_policy_assignment(
                policy_type="TenantDialPlan",
                display_name="US-DialPlan",
                assignment_type="group",
            ),
        ]

        mocks["user_service"].resolve_user.return_value = user
        mocks["policy_service"].get_user_policies.return_value = assignments

        result = runner.invoke(
            app, ["--no-color", "policies", "show", "john@contoso.com"]
        )
        assert result.exit_code == 0
        assert "John Doe" in result.stdout
        assert "AllowCalling" in result.stdout
        assert "US-DialPlan" in result.stdout
        assert "Direct" in result.stdout
        assert "Group" in result.stdout

    @pytest.mark.integration
    def test_show_user_policies_json_output(
        self,
        runner: CliRunner,
        mock_cli_services_policies_show: dict[str, MagicMock],
    ) -> None:
        """Show user policies with --json produces valid JSON."""
        mocks = mock_cli_services_policies_show
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        assignments = [
            make_policy_assignment(
                policy_type="TeamsCallingPolicy",
                display_name="AllowCalling",
                assignment_type="direct",
                policy_id="policy-123",
            ),
        ]

        mocks["user_service"].resolve_user.return_value = user
        mocks["policy_service"].get_user_policies.return_value = assignments

        result = runner.invoke(app, ["--json", "policies", "show", "john@contoso.com"])
        assert result.exit_code == 0

        data = json.loads(result.stdout)
        assert data["user_id"] == "user-12345"
        assert data["display_name"] == "John Doe"
        assert len(data["policies"]) == 1
        assert data["policies"][0]["policy_type"] == "TeamsCallingPolicy"
        assert data["policies"][0]["policy_name"] == "AllowCalling"
        assert data["policies"][0]["assignment_type"] == "direct"

    @pytest.mark.integration
    def test_show_user_policies_user_not_found(
        self,
        runner: CliRunner,
        mock_cli_services_policies_show: dict[str, MagicMock],
    ) -> None:
        """NotFoundError for user results in exit code 4."""
        mocks = mock_cli_services_policies_show
        mocks["user_service"].resolve_user.side_effect = NotFoundError(
            "User 'unknown@contoso.com' not found",
            remediation="Verify the user principal name is correct.",
        )

        result = runner.invoke(
            app, ["--no-color", "policies", "show", "unknown@contoso.com"]
        )
        assert result.exit_code == 4
        assert "not found" in result.stdout.lower()


class TestPoliciesAssignIntegration:
    """Integration tests for policies assign command."""

    @pytest.fixture
    def mock_cli_services_policies_assign(
        self,
        mock_user_service: MagicMock,
        mock_cache_manager: MagicMock,
        mock_auth_service: MagicMock,
    ) -> Generator[dict[str, MagicMock], None, None]:
        """Patch all services needed for policies assign command."""
        mock_gc = AsyncMock()
        mock_gc.__aenter__.return_value = mock_gc
        mock_gc.__aexit__.return_value = None

        # PolicyService needs assign_policy as AsyncMock
        mock_policy_service = MagicMock()
        mock_policy_service.assign_policy = AsyncMock(return_value=None)
        mock_policy_service.validate_policy.return_value = make_policy()

        with (
            patch(
                "teams_phone.cli.policies.CacheManager", return_value=mock_cache_manager
            ),
            patch(
                "teams_phone.cli.policies.AuthService", return_value=mock_auth_service
            ),
            patch("teams_phone.cli.policies.GraphClient", return_value=mock_gc),
            patch(
                "teams_phone.cli.policies.PolicyService",
                return_value=mock_policy_service,
            ),
            patch(
                "teams_phone.cli.policies.UserService", return_value=mock_user_service
            ),
            patch(
                "teams_phone.cli.policies._get_sync_service",
                return_value=mock_policy_service,
            ),
        ):
            yield {
                "cache_manager": mock_cache_manager,
                "auth_service": mock_auth_service,
                "graph_client": mock_gc,
                "policy_service": mock_policy_service,
                "user_service": mock_user_service,
            }

    @pytest.mark.integration
    def test_assign_policy_success(
        self,
        runner: CliRunner,
        mock_cli_services_policies_assign: dict[str, MagicMock],
    ) -> None:
        """Successful policy assignment with --force bypasses confirmation."""
        mocks = mock_cli_services_policies_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        policy = make_policy(
            policy_type="TeamsCallingPolicy",
            policy_name="AllowCalling",
            policy_id="policy-123",
        )

        mocks["user_service"].resolve_user.return_value = user
        mocks["policy_service"].validate_policy.return_value = policy
        mocks["policy_service"].assign_policy.return_value = None

        result = runner.invoke(
            app,
            [
                "--no-color",
                "policies",
                "assign",
                "john@contoso.com",
                "calling",
                "AllowCalling",
                "--force",
            ],
        )
        assert result.exit_code == 0
        assert "Assigned" in result.stdout
        assert "AllowCalling" in result.stdout
        assert "John Doe" in result.stdout

    @pytest.mark.integration
    def test_assign_policy_dry_run(
        self,
        runner: CliRunner,
        mock_cli_services_policies_assign: dict[str, MagicMock],
    ) -> None:
        """--dry-run previews without making changes."""
        mocks = mock_cli_services_policies_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        policy = make_policy(
            policy_type="TeamsCallingPolicy",
            policy_name="AllowCalling",
            policy_id="policy-123",
        )

        mocks["user_service"].resolve_user.return_value = user
        mocks["policy_service"].validate_policy.return_value = policy

        result = runner.invoke(
            app,
            [
                "--no-color",
                "policies",
                "assign",
                "john@contoso.com",
                "calling",
                "AllowCalling",
                "--dry-run",
            ],
        )
        assert result.exit_code == 0
        assert "Dry Run" in result.stdout
        assert "No changes made" in result.stdout

        # Verify assign_policy was NOT called
        mocks["policy_service"].assign_policy.assert_not_called()

    @pytest.mark.integration
    def test_assign_policy_dry_run_json(
        self,
        runner: CliRunner,
        mock_cli_services_policies_assign: dict[str, MagicMock],
    ) -> None:
        """--dry-run with --json produces valid JSON preview."""
        mocks = mock_cli_services_policies_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        policy = make_policy(
            policy_type="TeamsCallingPolicy",
            policy_name="AllowCalling",
            policy_id="policy-123",
        )

        mocks["user_service"].resolve_user.return_value = user
        mocks["policy_service"].validate_policy.return_value = policy

        result = runner.invoke(
            app,
            [
                "--json",
                "policies",
                "assign",
                "john@contoso.com",
                "calling",
                "AllowCalling",
                "--dry-run",
            ],
        )
        assert result.exit_code == 0

        data = json.loads(result.stdout)
        assert data["dry_run"] is True
        assert data["user_id"] == "user-12345"
        assert data["policy_type"] == "TeamsCallingPolicy"
        assert data["policy_name"] == "AllowCalling"

    @pytest.mark.integration
    def test_assign_policy_json_output(
        self,
        runner: CliRunner,
        mock_cli_services_policies_assign: dict[str, MagicMock],
    ) -> None:
        """Successful assignment with --json produces valid JSON."""
        mocks = mock_cli_services_policies_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        policy = make_policy(
            policy_type="TeamsCallingPolicy",
            policy_name="AllowCalling",
            policy_id="policy-123",
        )

        mocks["user_service"].resolve_user.return_value = user
        mocks["policy_service"].validate_policy.return_value = policy
        mocks["policy_service"].assign_policy.return_value = None

        result = runner.invoke(
            app,
            [
                "--json",
                "policies",
                "assign",
                "john@contoso.com",
                "calling",
                "AllowCalling",
            ],
        )
        assert result.exit_code == 0

        data = json.loads(result.stdout)
        assert data["success"] is True
        assert data["user_id"] == "user-12345"
        assert data["policy_type"] == "TeamsCallingPolicy"
        assert data["policy_name"] == "AllowCalling"

    @pytest.mark.integration
    def test_assign_policy_invalid_type(
        self,
        runner: CliRunner,
        mock_cli_services_policies_assign: dict[str, MagicMock],
    ) -> None:
        """Invalid policy type results in NotFoundError when service validates.

        Note: Unknown policy types are passed through to the service for
        validation, which returns NotFoundError (exit code 4).
        """
        mocks = mock_cli_services_policies_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        mocks["user_service"].resolve_user.return_value = user
        mocks["policy_service"].validate_policy.side_effect = NotFoundError(
            "Unknown policy type: invalidtype",
            remediation="Valid types are: calling, voicemail, emergency, etc.",
        )

        result = runner.invoke(
            app,
            [
                "--no-color",
                "policies",
                "assign",
                "john@contoso.com",
                "invalidtype",
                "SomePolicy",
                "--force",
            ],
        )
        assert result.exit_code == 4
        assert (
            "not found" in result.stdout.lower() or "unknown" in result.stdout.lower()
        )

    @pytest.mark.integration
    def test_assign_policy_user_not_found(
        self,
        runner: CliRunner,
        mock_cli_services_policies_assign: dict[str, MagicMock],
    ) -> None:
        """NotFoundError for user results in exit code 4."""
        mocks = mock_cli_services_policies_assign
        mocks["user_service"].resolve_user.side_effect = NotFoundError(
            "User 'unknown@contoso.com' not found",
            remediation="Verify the user principal name is correct.",
        )

        result = runner.invoke(
            app,
            [
                "--no-color",
                "policies",
                "assign",
                "unknown@contoso.com",
                "calling",
                "AllowCalling",
                "--force",
            ],
        )
        assert result.exit_code == 4
        assert "not found" in result.stdout.lower()

    @pytest.mark.integration
    def test_assign_policy_not_found(
        self,
        runner: CliRunner,
        mock_cli_services_policies_assign: dict[str, MagicMock],
    ) -> None:
        """NotFoundError for policy results in exit code 4."""
        mocks = mock_cli_services_policies_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        mocks["user_service"].resolve_user.return_value = user
        mocks["policy_service"].validate_policy.side_effect = NotFoundError(
            "Policy 'NonExistent' not found for type 'TeamsCallingPolicy'",
            remediation="Run 'policies list calling' to see available policies.",
        )

        result = runner.invoke(
            app,
            [
                "--no-color",
                "policies",
                "assign",
                "john@contoso.com",
                "calling",
                "NonExistent",
                "--force",
            ],
        )
        assert result.exit_code == 4
        assert "not found" in result.stdout.lower()
