"""Integration tests for CLI global options and error scenarios.

This module tests global flag behavior (--tenant, --verbose, --no-color, --json)
and comprehensive error scenario testing across all exception types.
"""

from __future__ import annotations

import json
from unittest.mock import MagicMock

import pytest
from typer.testing import CliRunner

from teams_phone.cli.main import app
from teams_phone.exceptions import (
    AuthenticationError,
    ConfigurationError,
    ContractError,
    NotFoundError,
    ValidationError,
)

from tests.integration.cli.conftest import make_user_config


class TestGlobalOptions:
    """Integration tests for global CLI options."""

    @pytest.mark.integration
    def test_json_flag_position_before_command(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """--json flag before command produces JSON output."""
        mocks = mock_cli_services_users
        users = [
            make_user_config(upn="john@contoso.com", display_name="John Doe"),
        ]
        mocks["user_service"].list_users.return_value = users

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

        data = json.loads(result.stdout)
        assert isinstance(data, list)
        assert len(data) == 1
        assert data[0]["user_principal_name"] == "john@contoso.com"

    @pytest.mark.integration
    def test_no_color_flag_disables_ansi(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """--no-color flag suppresses ANSI escape codes."""
        mocks = mock_cli_services_users
        users = [
            make_user_config(upn="john@contoso.com", display_name="John Doe"),
        ]
        mocks["user_service"].list_users.return_value = users

        result = runner.invoke(app, ["--no-color", "users", "list"])
        assert result.exit_code == 0
        # Check output contains the data without ANSI escape sequences
        assert "john@contoso.com" in result.stdout or "John" in result.stdout
        # The output should be plain text

    @pytest.mark.integration
    def test_verbose_flag_increases_output(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """--verbose flag enables verbose mode in context."""
        mocks = mock_cli_services_users
        users = [
            make_user_config(upn="john@contoso.com", display_name="John Doe"),
        ]
        mocks["user_service"].list_users.return_value = users

        result = runner.invoke(app, ["--verbose", "--no-color", "users", "list"])
        assert result.exit_code == 0
        # Verbose mode should not cause failures
        assert "john@contoso.com" in result.stdout or "John" in result.stdout

    @pytest.mark.integration
    def test_combined_flags(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """Multiple global flags work together."""
        mocks = mock_cli_services_users
        users = [
            make_user_config(upn="john@contoso.com", display_name="John Doe"),
        ]
        mocks["user_service"].list_users.return_value = users

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

        data = json.loads(result.stdout)
        assert isinstance(data, list)
        assert data[0]["user_principal_name"] == "john@contoso.com"

    @pytest.mark.integration
    def test_json_flag_on_empty_result(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """--json flag with empty result returns empty array."""
        mocks = mock_cli_services_users
        mocks["user_service"].list_users.return_value = []

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

        data = json.loads(result.stdout)
        assert isinstance(data, list)
        assert len(data) == 0


class TestErrorScenarios:
    """Integration tests for error scenarios and exit codes."""

    @pytest.mark.integration
    def test_not_found_error_exit_code_4(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """NotFoundError produces exit code 4."""
        mocks = mock_cli_services_users
        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", "users", "show", "unknown@contoso.com"]
        )
        assert result.exit_code == 4
        assert "not found" in result.stdout.lower()

    @pytest.mark.integration
    def test_validation_error_exit_code_5(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """ValidationError produces exit code 5."""
        mocks = mock_cli_services_users
        mocks["user_service"].list_users.side_effect = ValidationError(
            "Invalid filter combination",
            remediation="Use either --licensed or --unlicensed, not both.",
        )

        result = runner.invoke(app, ["--no-color", "users", "list"])
        assert result.exit_code == 5
        assert "invalid" in result.stdout.lower()

    @pytest.mark.integration
    def test_authentication_error_exit_code_2(
        self,
        runner: CliRunner,
        mock_cli_services_auth: dict[str, MagicMock],
    ) -> None:
        """AuthenticationError produces exit code 2."""
        mocks = mock_cli_services_auth
        mocks["auth_service"].get_status.side_effect = AuthenticationError(
            "Token expired",
            remediation="Run 'teams-phone auth login' to authenticate.",
        )

        result = runner.invoke(app, ["--no-color", "auth", "status"])
        assert result.exit_code == 2
        assert "expired" in result.stdout.lower() or "Token" in result.stdout

    @pytest.mark.integration
    def test_configuration_error_exit_code_7(
        self,
        runner: CliRunner,
        mock_cli_services_locations: dict[str, MagicMock],
    ) -> None:
        """ConfigurationError produces exit code 7."""
        mocks = mock_cli_services_locations
        mocks["location_service"].list_locations.side_effect = ConfigurationError(
            "Cache file not found",
            remediation="Run 'Export-Locations.ps1' to create the cache.",
        )

        result = runner.invoke(app, ["--no-color", "locations", "list"])
        assert result.exit_code == 7
        assert "not found" in result.stdout.lower() or "Cache" in result.stdout

    @pytest.mark.integration
    def test_contract_error_exit_code_8(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """ContractError produces exit code 8."""
        mocks = mock_cli_services_users
        mocks["user_service"].list_users.side_effect = ContractError(
            "Unexpected response format from Graph API",
            remediation="This may indicate an API change. Please report this issue.",
        )

        result = runner.invoke(app, ["--no-color", "users", "list"])
        assert result.exit_code == 8
        assert "response" in result.stdout.lower() or "API" in result.stdout

    @pytest.mark.integration
    def test_error_exit_code_in_json_mode(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """Errors in JSON mode return correct exit code without text output.

        In JSON mode, error messages are suppressed (no output) and the
        exit code is the only indicator of failure. This is by design
        to keep JSON output machine-parseable.
        """
        mocks = mock_cli_services_users
        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, ["--json", "users", "show", "unknown@contoso.com"])
        # Exit code indicates the error type
        assert result.exit_code == 4
        # In JSON mode, error messages are not output to keep output parseable
        # The empty output is intentional - scripts should check exit code

    @pytest.mark.integration
    def test_remediation_message_displayed(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """Remediation message is displayed with error."""
        mocks = mock_cli_services_users
        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", "users", "show", "unknown@contoso.com"]
        )
        assert result.exit_code == 4
        assert "not found" in result.stdout.lower()

    @pytest.mark.integration
    def test_mutually_exclusive_options_validation_error(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """Mutually exclusive options produce ValidationError exit code 5."""
        result = runner.invoke(
            app, ["--no-color", "users", "list", "--limit", "10", "--all"]
        )
        assert result.exit_code == 5
        assert "Cannot specify both" in result.stdout

    @pytest.mark.integration
    def test_numbers_list_validation_error(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """Numbers list with conflicting options produces exit code 5."""
        result = runner.invoke(
            app, ["--no-color", "numbers", "list", "--limit", "10", "--all"]
        )
        assert result.exit_code == 5
        assert "Cannot specify both" in result.stdout

    @pytest.mark.integration
    def test_locations_list_validation_error(
        self,
        runner: CliRunner,
        mock_cli_services_locations: dict[str, MagicMock],
    ) -> None:
        """Locations list with conflicting options produces exit code 5."""
        result = runner.invoke(
            app, ["--no-color", "locations", "list", "--limit", "10", "--all"]
        )
        assert result.exit_code == 5
        assert "Cannot specify both" in result.stdout


class TestGlobalTenantFlag:
    """Integration tests for --tenant global flag."""

    @pytest.mark.integration
    def test_tenant_flag_is_passed_to_context(
        self,
        runner: CliRunner,
        mock_cli_services_users: dict[str, MagicMock],
    ) -> None:
        """--tenant flag is captured in CLI context."""
        mocks = mock_cli_services_users
        users = [
            make_user_config(upn="john@contoso.com", display_name="John Doe"),
        ]
        mocks["user_service"].list_users.return_value = users

        result = runner.invoke(
            app, ["--tenant", "my-tenant", "--no-color", "users", "list"]
        )
        # Command should succeed regardless of tenant value
        # (tenant is used by ConfigManager which is mocked)
        assert result.exit_code == 0
