"""Unit tests for CLI locations commands."""

from __future__ import annotations

import json
from unittest.mock import MagicMock, patch

import pytest
from tests.conftest import CliRunner

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

runner = CliRunner()


# Test fixtures
def _make_location(
    location_id: str = "loc-11111111-1111-1111-1111-111111111111",
    civic_address_id: str = "civic-22222222-2222-2222-2222-222222222222",
    description: str = "Seattle HQ",
    company_name: str = "Contoso",
    address: str = "123 Main St",
    city: str = "Seattle",
    state_or_province: str = "WA",
    postal_code: str = "98101",
    country_or_region: str = "US",
    is_default: bool = True,
) -> EmergencyLocation:
    """Create an EmergencyLocation for testing."""
    # Use alias names as required by Pydantic with strict mode
    return EmergencyLocation.model_validate(
        {
            "locationId": location_id,
            "civicAddressId": civic_address_id,
            "description": description,
            "companyName": company_name,
            "address": address,
            "city": city,
            "stateOrProvince": state_or_province,
            "postalCode": postal_code,
            "countryOrRegion": country_or_region,
            "isDefault": is_default,
        }
    )


class TestLocationsListCommand:
    """Tests for locations list command."""

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

    @pytest.mark.unit
    def test_list_empty(self) -> None:
        """list shows empty message when no locations found."""
        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = []
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

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

    @pytest.mark.unit
    def test_list_shows_locations(self) -> None:
        """list shows locations in table format."""
        locations = [
            _make_location(description="Seattle HQ", city="Seattle"),
            _make_location(
                location_id="loc-2",
                description="Portland Office",
                city="Portland",
            ),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["--no-color", "locations", "list"])
            assert result.exit_code == 0
            assert "Seattle HQ" in result.stdout
            assert "Portland Office" in result.stdout

    @pytest.mark.unit
    def test_list_json_output(self) -> None:
        """list outputs valid JSON with --json flag."""
        locations = [_make_location()]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "locations", "list"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert len(data) == 1
            assert data[0]["description"] == "Seattle HQ"
            assert data[0]["city"] == "Seattle"
            assert data[0]["is_default"] is True

    @pytest.mark.unit
    def test_list_json_empty(self) -> None:
        """list outputs empty JSON array when no locations."""
        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = []
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "locations", "list"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data == []

    @pytest.mark.unit
    def test_list_with_city_filter(self) -> None:
        """list filters locations by city."""
        locations = [
            _make_location(description="Seattle HQ", city="Seattle"),
            _make_location(
                location_id="loc-2",
                description="Portland Office",
                city="Portland",
            ),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "locations", "list", "--city", "Seattle"]
            )
            assert result.exit_code == 0
            assert "Seattle HQ" in result.stdout
            assert "Portland Office" not in result.stdout

    @pytest.mark.unit
    def test_list_city_filter_case_insensitive(self) -> None:
        """list city filter is case insensitive."""
        locations = [
            _make_location(description="Seattle HQ", city="Seattle"),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "locations", "list", "--city", "seattle"]
            )
            assert result.exit_code == 0
            assert "Seattle HQ" in result.stdout

    @pytest.mark.unit
    def test_list_with_limit(self) -> None:
        """list respects --limit option."""
        locations = [
            _make_location(location_id="loc-1", description="Location 1"),
            _make_location(location_id="loc-2", description="Location 2"),
            _make_location(location_id="loc-3", description="Location 3"),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "locations", "list", "--limit", "2"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert len(data) == 2

    @pytest.mark.unit
    def test_list_limit_and_all_mutually_exclusive(self) -> None:
        """list errors when both --limit and --all are specified."""
        result = runner.invoke(app, ["locations", "list", "--limit", "10", "--all"])
        assert result.exit_code == 5  # ValidationError exit code
        assert "Cannot specify both --limit and --all" in result.stdout

    @pytest.mark.unit
    def test_list_shows_staleness_warning(self) -> None:
        """list shows staleness warning when cache is stale."""
        locations = [_make_location()]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = (
                "Location cache is 45 days old (threshold: 30 days). "
                "Consider refreshing by running the PowerShell export script."
            )
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["--no-color", "locations", "list"])
            assert result.exit_code == 0
            # Check for key parts of the warning message (ANSI codes may affect literal match)
            assert "days old" in result.stdout
            assert "threshold" in result.stdout


class TestLocationsShowCommand:
    """Tests for locations show command."""

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

    @pytest.mark.unit
    def test_show_requires_location_argument(self) -> None:
        """show requires location argument."""
        result = runner.invoke(app, ["locations", "show"])
        assert result.exit_code == 2
        output = result.stdout + (result.stderr or "")
        assert (
            "LOCATION" in output
            or "Missing argument" in output
            or result.exit_code == 2
        )

    @pytest.mark.unit
    def test_show_displays_location(self) -> None:
        """show displays location details."""
        location = _make_location()

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.validate_location.return_value = location
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "locations", "show", "Seattle HQ"]
            )
            assert result.exit_code == 0
            assert "Seattle HQ" in result.stdout
            # Check for parts of address (ANSI codes may affect literal match)
            assert "Main St" in result.stdout
            assert "Seattle" in result.stdout
            assert "Contoso" in result.stdout

    @pytest.mark.unit
    def test_show_json_output(self) -> None:
        """show outputs valid JSON with --json flag."""
        location = _make_location()

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.validate_location.return_value = location
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "locations", "show", "Seattle HQ"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["description"] == "Seattle HQ"
            assert data["address"] == "123 Main St"
            assert data["city"] == "Seattle"
            assert data["is_default"] is True

    @pytest.mark.unit
    def test_show_not_found(self) -> None:
        """show exits with error when location not found."""
        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.validate_location.side_effect = NotFoundError(
                "Location 'nonexistent' not found",
                remediation="Verify the location ID or description is correct.",
            )
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["locations", "show", "nonexistent"])
            assert result.exit_code == 4  # NotFoundError exit code

    @pytest.mark.unit
    def test_show_staleness_warning(self) -> None:
        """show displays staleness warning when cache is stale."""
        location = _make_location()

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.validate_location.return_value = location
            mock_service.get_staleness_warning.return_value = (
                "Location cache not found. "
                "Run the PowerShell export script to create the cache."
            )
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "locations", "show", "Seattle HQ"]
            )
            assert result.exit_code == 0
            assert "cache not found" in result.stdout


class TestLocationsSearchCommand:
    """Tests for locations search command."""

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

    @pytest.mark.unit
    def test_search_requires_query_argument(self) -> None:
        """search requires query argument."""
        result = runner.invoke(app, ["locations", "search"])
        assert result.exit_code == 2
        output = result.stdout + (result.stderr or "")
        assert (
            "QUERY" in output or "Missing argument" in output or result.exit_code == 2
        )

    @pytest.mark.unit
    def test_search_shows_results(self) -> None:
        """search shows matching locations."""
        locations = [
            _make_location(description="Seattle HQ", city="Seattle"),
            _make_location(
                location_id="loc-2",
                description="Seattle Satellite",
                city="Seattle",
            ),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "locations", "search", "Seattle"]
            )
            assert result.exit_code == 0
            assert "Seattle HQ" in result.stdout
            assert "Seattle Satellite" in result.stdout

    @pytest.mark.unit
    def test_search_no_results(self) -> None:
        """search shows message when no locations found."""
        locations = [_make_location(description="Seattle HQ", city="Seattle")]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "locations", "search", "nonexistent"]
            )
            assert result.exit_code == 0
            assert "No locations found matching" in result.stdout
            assert "nonexistent" in result.stdout

    @pytest.mark.unit
    def test_search_json_output(self) -> None:
        """search outputs valid JSON with --json flag."""
        locations = [_make_location(description="Seattle HQ")]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "locations", "search", "Seattle"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert len(data) == 1
            assert data[0]["description"] == "Seattle HQ"

    @pytest.mark.unit
    def test_search_json_empty(self) -> None:
        """search outputs empty JSON array when no results."""
        locations: list[EmergencyLocation] = []

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "locations", "search", "anything"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data == []

    @pytest.mark.unit
    def test_search_with_limit(self) -> None:
        """search respects --limit option."""
        locations = [
            _make_location(location_id="loc-1", description="Seattle 1"),
            _make_location(location_id="loc-2", description="Seattle 2"),
            _make_location(location_id="loc-3", description="Seattle 3"),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app, ["--json", "locations", "search", "Seattle", "--limit", "2"]
            )
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert len(data) == 2

    @pytest.mark.unit
    def test_search_default_limit(self) -> None:
        """search uses default limit of 25."""
        # Create 30 locations
        locations = [
            _make_location(location_id=f"loc-{i}", description=f"Seattle {i}")
            for i in range(30)
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "locations", "search", "Seattle"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert len(data) == 25

    @pytest.mark.unit
    def test_search_with_field_name(self) -> None:
        """search with --field name only searches description."""
        locations = [
            _make_location(description="Seattle HQ", city="Portland"),
            _make_location(
                location_id="loc-2",
                description="Portland Office",
                city="Seattle",
            ),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app,
                ["--no-color", "locations", "search", "Seattle", "--field", "name"],
            )
            assert result.exit_code == 0
            assert "Seattle HQ" in result.stdout
            # Portland Office has Seattle in city, but we're only searching name
            assert "Portland Office" not in result.stdout

    @pytest.mark.unit
    def test_search_with_field_city(self) -> None:
        """search with --field city only searches city."""
        locations = [
            _make_location(description="Seattle HQ", city="Seattle"),
            _make_location(
                location_id="loc-2",
                description="Portland Office",
                city="Portland",
            ),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app,
                ["--no-color", "locations", "search", "Seattle", "--field", "city"],
            )
            assert result.exit_code == 0
            assert "Seattle HQ" in result.stdout
            assert "Portland Office" not in result.stdout

    @pytest.mark.unit
    def test_search_with_field_address(self) -> None:
        """search with --field address only searches address."""
        locations = [
            _make_location(description="HQ", address="123 Main St", city="Seattle"),
            _make_location(
                location_id="loc-2",
                description="Office",
                address="456 Oak Ave",
                city="Seattle",
            ),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app,
                ["--no-color", "locations", "search", "Main", "--field", "address"],
            )
            assert result.exit_code == 0
            assert "HQ" in result.stdout
            assert "Office" not in result.stdout

    @pytest.mark.unit
    def test_search_with_field_company(self) -> None:
        """search with --field company only searches company name."""
        locations = [
            _make_location(description="HQ", company_name="Contoso"),
            _make_location(
                location_id="loc-2",
                description="Office",
                company_name="Fabrikam",
            ),
        ]

        with (
            patch("teams_phone.cli.locations.CacheManager") as mock_cm_class,
            patch("teams_phone.cli.locations.LocationService") as mock_ls_class,
        ):
            mock_cm = MagicMock()
            mock_cm_class.return_value = mock_cm

            mock_service = MagicMock()
            mock_service.list_locations.return_value = locations
            mock_service.get_staleness_warning.return_value = None
            mock_ls_class.return_value = mock_service

            result = runner.invoke(
                app,
                ["--no-color", "locations", "search", "Contoso", "--field", "company"],
            )
            assert result.exit_code == 0
            assert "HQ" in result.stdout
            assert "Office" not in result.stdout


class TestLocationsCommandIntegration:
    """Integration tests for locations command group."""

    @pytest.mark.unit
    def test_locations_commands_appear_in_help(self) -> None:
        """All locations commands appear in locations --help."""
        result = runner.invoke(app, ["locations", "--help"])
        assert result.exit_code == 0
        assert "list" in result.stdout
        assert "show" in result.stdout
        assert "search" in result.stdout

    @pytest.mark.unit
    def test_locations_no_args_shows_help(self) -> None:
        """locations with no args shows help (exit code 2)."""
        result = runner.invoke(app, ["locations"])
        assert result.exit_code == 2
        assert "Usage:" in result.stdout

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