"""
End-to-End tests for Home Assistant Update Management tools.

This test suite validates the update management tools including:
- ha_get_updates: List all updates or get details for a specific update (consolidated tool)
- ha_get_overview: Get system version and info (includes entity overview)

Tests are designed for Docker Home Assistant test environment.
"""

import logging

import pytest

# Import test utilities
from ...utilities.assertions import MCPAssertions, safe_call_tool

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


@pytest.mark.updates
class TestUpdateManagement:
    """Test suite for update management tools."""

    async def test_list_updates_basic(self, mcp_client):
        """
        Test: Basic listing of available updates.

        Validates that ha_get_updates returns the expected structure
        even when no updates are available (test environment typically up-to-date).
        """
        logger.info("Testing basic update listing...")

        async with MCPAssertions(mcp_client) as mcp:
            # Call ha_get_updates
            result = await mcp.call_tool_success(
                "ha_get_updates",
                {"include_skipped": False},
            )

            # Verify response structure
            assert "success" in result, f"Missing 'success' field in result: {result}"
            assert result.get("success") is True, f"Expected success=True: {result}"
            assert (
                "updates_available" in result
            ), f"Missing 'updates_available' field: {result}"
            assert "updates" in result, f"Missing 'updates' field: {result}"
            assert "categories" in result, f"Missing 'categories' field: {result}"

            # updates_available should be a non-negative integer
            updates_count = result.get("updates_available", -1)
            assert (
                isinstance(updates_count, int) and updates_count >= 0
            ), f"Invalid updates_available value: {updates_count}"

            logger.info(f"Found {updates_count} available updates")

            # Verify updates list structure if any updates exist
            updates_list = result.get("updates", [])
            assert isinstance(
                updates_list, list
            ), f"Updates should be a list: {type(updates_list)}"

            if updates_list:
                # Check first update has expected fields
                first_update = updates_list[0]
                expected_fields = [
                    "entity_id",
                    "title",
                    "installed_version",
                    "latest_version",
                    "category",
                ]
                for field in expected_fields:
                    assert (
                        field in first_update
                    ), f"Update missing field '{field}': {first_update}"
                logger.info(f"First update: {first_update.get('title')}")

            # Verify categories structure
            categories = result.get("categories", {})
            assert isinstance(
                categories, dict
            ), f"Categories should be a dict: {type(categories)}"

            # Valid category keys
            valid_categories = {
                "core",
                "os",
                "supervisor",
                "addons",
                "hacs",
                "devices",
                "other",
            }
            for cat_key in categories.keys():
                assert (
                    cat_key in valid_categories
                ), f"Unknown category '{cat_key}': {categories.keys()}"

            logger.info("Basic update listing test passed")

    async def test_list_updates_with_skipped(self, mcp_client):
        """
        Test: Listing updates with skipped updates included.

        Validates that include_skipped parameter works correctly.
        """
        logger.info("Testing update listing with skipped updates...")

        async with MCPAssertions(mcp_client) as mcp:
            # Call with include_skipped=True
            result_with_skipped = await mcp.call_tool_success(
                "ha_get_updates",
                {"include_skipped": True},
            )

            # Call without skipped (default)
            result_without_skipped = await mcp.call_tool_success(
                "ha_get_updates",
                {"include_skipped": False},
            )

            # Verify both succeeded
            assert result_with_skipped.get("success") is True
            assert result_without_skipped.get("success") is True

            # include_skipped flag should be reflected in response
            assert result_with_skipped.get("include_skipped") is True
            assert result_without_skipped.get("include_skipped") is False

            # skipped_count should be present
            assert "skipped_count" in result_with_skipped
            assert "skipped_count" in result_without_skipped

            logger.info(
                f"With skipped: {len(result_with_skipped.get('updates', []))} updates, "
                f"Without skipped: {len(result_without_skipped.get('updates', []))} updates"
            )
            logger.info("Skipped updates test passed")

    async def test_get_system_overview(self, mcp_client):
        """
        Test: Get system version and configuration info via ha_get_overview.

        Validates that ha_get_overview returns expected system information
        (replaces ha_get_system_version).
        """
        logger.info("Testing system overview retrieval...")

        async with MCPAssertions(mcp_client) as mcp:
            # Call ha_get_overview
            result = await mcp.call_tool_success(
                "ha_get_overview",
                {},
            )

            # Verify response structure
            assert result.get("success") is True, f"Expected success=True: {result}"

            # Verify system_info field exists
            assert "system_info" in result, f"Missing 'system_info' field: {result}"
            system_info = result["system_info"]

            # Required fields in system_info
            assert "version" in system_info, f"Missing 'version' field: {system_info}"
            version = system_info.get("version")
            assert version is not None, "Version should not be None"
            assert isinstance(
                version, str
            ), f"Version should be string: {type(version)}"

            # Version format validation (should be like "2025.1.0")
            version_parts = version.split(".")
            assert (
                len(version_parts) >= 2
            ), f"Version should have at least 2 parts: {version}"

            logger.info(f"Home Assistant version: {version}")

            # Other expected fields in system_info
            optional_fields = [
                "location_name",
                "time_zone",
                "config_dir",
                "components_loaded",
            ]
            for field in optional_fields:
                if field in system_info:
                    logger.info(f"  {field}: {system_info[field]}")

            # components_loaded should be a positive integer
            components = system_info.get("components_loaded")
            if components is not None:
                assert (
                    isinstance(components, int) and components > 0
                ), f"Invalid components_loaded: {components}"

            logger.info("System version test passed")

    async def test_get_updates_invalid_entity(self, mcp_client):
        """
        Test: Get update details with invalid entity ID.

        Validates that ha_get_updates handles invalid entity IDs gracefully.
        """
        logger.info("Testing update details with invalid entity...")

        async with MCPAssertions(mcp_client) as mcp:
            # Test with non-existent entity
            await mcp.call_tool_failure(
                "ha_get_updates",
                {"entity_id": "update.nonexistent_entity_xyz"},
                expected_error="not found",
            )
            logger.info("Non-existent entity test passed")

            # Test with invalid format (not starting with "update.")
            await mcp.call_tool_failure(
                "ha_get_updates",
                {"entity_id": "light.invalid_entity"},
                expected_error="Invalid entity_id format",
            )
            logger.info("Invalid format test passed")

    async def test_get_update_details(self, mcp_client):
        """
        Test: Get detailed info for an actual update entity.

        This test first lists updates to find available update entities,
        then attempts to get details (including release notes) for them.
        """
        logger.info("Testing update details for actual update entities...")

        async with MCPAssertions(mcp_client) as mcp:
            # First, list all update entities
            list_result = await mcp.call_tool_success(
                "ha_get_updates",
                {"include_skipped": True},
            )

            updates = list_result.get("updates", [])

            if not updates:
                # No updates available, search for update entities directly
                search_result = await mcp.call_tool_success(
                    "ha_search_entities",
                    {"query": "update", "domain_filter": "update", "limit": 5},
                )

                search_data = search_result.get("data", search_result)
                results = search_data.get("results", [])

                if not results:
                    logger.info(
                        "No update entities found in test environment, skipping"
                    )
                    return

                # Use first found update entity
                entity_id = results[0].get("entity_id", "")
            else:
                # Use first available update
                entity_id = updates[0].get("entity_id", "")

            if not entity_id:
                logger.info("Could not find update entity to test, skipping")
                return

            logger.info(f"Testing update details for: {entity_id}")

            # Get details for this entity (consolidated ha_get_updates with entity_id)
            result = await mcp.call_tool_success(
                "ha_get_updates",
                {"entity_id": entity_id},
            )

            # Verify response structure
            assert result.get("success") is True, f"Expected success=True: {result}"
            assert (
                result.get("entity_id") == entity_id
            ), f"Entity ID mismatch: {result.get('entity_id')} != {entity_id}"
            assert "latest_version" in result, f"Missing 'latest_version' field: {result}"

            # release_notes may be None if not available
            release_notes = result.get("release_notes")
            source = result.get("release_notes_source")

            if release_notes:
                logger.info(f"Got release notes from source: {source}")
                logger.info(
                    f"Release notes preview: {release_notes[:200]}..."
                    if len(release_notes) > 200
                    else f"Release notes: {release_notes}"
                )
            else:
                logger.info(
                    f"No release notes available for {entity_id}: "
                    f"{result.get('release_notes_hint', 'No hint')}"
                )

            logger.info("Update details test passed")


@pytest.mark.updates
class TestUpdateToolsEdgeCases:
    """Test edge cases and error handling for update tools."""

    async def test_list_updates_response_consistency(self, mcp_client):
        """
        Test: Verify update listing response is consistent across calls.

        Multiple calls should return consistent structure and similar data.
        """
        logger.info("Testing update listing consistency...")

        async with MCPAssertions(mcp_client) as mcp:
            # Make two calls
            result1 = await mcp.call_tool_success("ha_get_updates", {})
            result2 = await mcp.call_tool_success("ha_get_updates", {})

            # Both should have same structure
            assert result1.get("success") == result2.get("success")
            assert "updates_available" in result1 and "updates_available" in result2
            assert "categories" in result1 and "categories" in result2

            # Update counts should be same or very similar
            # (small differences possible if update status changes between calls)
            count1 = result1.get("updates_available", 0)
            count2 = result2.get("updates_available", 0)
            assert (
                abs(count1 - count2) <= 1
            ), f"Update counts differ significantly: {count1} vs {count2}"

            logger.info("Consistency test passed")

    async def test_system_version_fields_presence(self, mcp_client):
        """
        Test: Verify all expected system version fields are present.

        Validates comprehensive field coverage in system version response.
        """
        logger.info("Testing system version field presence...")

        async with MCPAssertions(mcp_client) as mcp:
            result = await mcp.call_tool_success("ha_get_overview", {})

            # Verify system_info exists
            assert "system_info" in result, "Missing system_info field"
            system_info = result["system_info"]

            # Core required fields in system_info
            required_fields = ["version"]
            for field in required_fields:
                assert (
                    field in system_info
                ), f"Required field '{field}' missing from system_info: {system_info.keys()}"

            # Common optional fields that should usually be present
            common_fields = [
                "location_name",
                "time_zone",
                "components_loaded",
            ]
            present_fields = [f for f in common_fields if f in system_info]
            logger.info(f"Present optional fields: {present_fields}")

            # At least some optional fields should be present
            assert (
                len(present_fields) >= 1
            ), f"Expected at least 1 optional field present: {common_fields}"

            logger.info("Field presence test passed")

    async def test_update_categorization(self, mcp_client):
        """
        Test: Verify update entities are correctly categorized.

        Tests the categorization logic for different update types.
        """
        logger.info("Testing update categorization logic...")

        async with MCPAssertions(mcp_client) as mcp:
            result = await mcp.call_tool_success(
                "ha_get_updates", {"include_skipped": True}
            )

            updates = result.get("updates", [])
            categories = result.get("categories", {})

            # Every update in the list should have a category
            for update in updates:
                assert (
                    "category" in update
                ), f"Update missing category: {update.get('entity_id')}"
                category = update.get("category")
                assert category in {
                    "core",
                    "os",
                    "supervisor",
                    "addons",
                    "hacs",
                    "devices",
                    "other",
                }, f"Invalid category '{category}' for {update.get('entity_id')}"

            # Categories dict should only contain updates that match
            for cat_name, cat_updates in categories.items():
                for update in cat_updates:
                    assert update.get("category") == cat_name, (
                        f"Update {update.get('entity_id')} in category {cat_name} "
                        f"but has category {update.get('category')}"
                    )

            logger.info(
                f"Verified categorization for {len(updates)} updates "
                f"across {len(categories)} categories"
            )
            logger.info("Categorization test passed")


@pytest.mark.updates
class TestIncludeReleaseNotes:
    """Test suite for ha_get_updates include_release_notes parameter."""

    async def test_include_release_notes_response_structure(self, mcp_client):
        """
        Test: ha_get_updates with include_release_notes returns expected structure.

        The Docker test environment may not have a Core update entity.
        When present, validates the full response structure including
        breaking_changes and installed_integrations fields.
        """
        logger.info("Testing ha_get_updates with include_release_notes...")

        # First find the Core update entity
        result = await safe_call_tool(
            mcp_client,
            "ha_get_updates",
            {"include_skipped": True},
        )

        if not result.get("success"):
            logger.info("Could not list updates, skipping")
            return

        # Find core update entity
        core_entity_id = None
        for update in result.get("updates", []):
            if update.get("category") == "core":
                core_entity_id = update.get("entity_id")
                break

        if not core_entity_id:
            logger.info("No Core update entity in test env (expected)")
            return

        # Get details with include_release_notes
        detail_result = await safe_call_tool(
            mcp_client,
            "ha_get_updates",
            {"entity_id": core_entity_id, "include_release_notes": True},
        )

        assert detail_result.get("success") is True, f"Expected success=True: {detail_result}"
        assert detail_result.get("category") == "core"

        # If an update is available, verify breaking changes structure
        if detail_result.get("update_available"):
            assert "installed_integrations" in detail_result
            assert "multi_version_release_notes" in detail_result
            assert "breaking_changes" in detail_result

            assert isinstance(detail_result["installed_integrations"], list)
            assert isinstance(detail_result["multi_version_release_notes"], list)
            assert isinstance(detail_result["breaking_changes"], dict)

            bc = detail_result["breaking_changes"]
            assert "entries" in bc and "count" in bc and "versions_checked" in bc
            assert isinstance(bc["entries"], list)
            assert isinstance(bc["count"], int)

            logger.info(
                f"Release notes: {detail_result.get('installed_version')} -> "
                f"{detail_result.get('latest_version')}, "
                f"{bc['count']} breaking changes, "
                f"{len(detail_result['multi_version_release_notes'])} release notes, "
                f"{len(detail_result['installed_integrations'])} integrations"
            )
        else:
            logger.info("No Core update available, breaking changes fields may be absent")

        logger.info("include_release_notes test passed")


async def test_update_tools_discovery(mcp_client):
    """
    Test: Verify update tools are discoverable and registered.

    Validates that update tools are available in the MCP server.
    """
    logger.info("Testing update tools discovery...")

    # Get available tools - FastMCP returns a list directly
    tools = await mcp_client.list_tools()

    # Handle both list and object response types
    if hasattr(tools, "tools"):
        tool_list = tools.tools
    else:
        tool_list = tools

    # Convert to list of tool names
    tool_names = [tool.name for tool in tool_list]

    # Check that update tools are registered
    expected_tools = [
        "ha_get_updates",  # Handles list, get-by-entity_id, and release notes modes
        "ha_get_overview",  # System info and version
    ]

    for tool_name in expected_tools:
        assert tool_name in tool_names, (
            f"Tool '{tool_name}' not found in registered tools. "
            f"Available tools: {sorted(tool_names)}"
        )
        logger.info(f"Found tool: {tool_name}")

    # ha_check_update_notes should NOT be registered (consolidated into ha_get_updates)
    assert "ha_check_update_notes" not in tool_names, (
        "ha_check_update_notes should be consolidated into ha_get_updates"
    )

    logger.info("Update tools discovery test passed")


@pytest.mark.updates
class TestUpdatesGetNegativeInputs:
    """
    A2 negative-input tests for ha_get_updates' single-entity detail mode.

    Covers the nonexistent-entity_id failure path. Existing tests in this
    file exercise listing, release-notes inclusion, and edge cases on real
    update entities, but do not call ha_get_updates with an entity_id that
    has no matching update.

    Methodology: source-verified against tools_updates.py. _get_update_details
    fetches the entity state via REST; a 404 from /api/states/<entity_id>
    raises HomeAssistantAPIError("API error: 404 - Entity not found.").
    The except-Exception branch in ha_get_updates matches "404" / "not found"
    in the error message and raises ErrorCode.ENTITY_NOT_FOUND. Live-probed
    against a real HA instance: GET /api/states/update.<nonexistent> returns
    HTTP 404 with body {"message": "Entity not found."}.
    """

    async def test_get_updates_nonexistent_entity_id(self, mcp_client):
        """
        Test: ha_get_updates(entity_id="update.<nonexistent>") returns a
        structured error with code ENTITY_NOT_FOUND, not success=True.

        Source path: REST 404 → HomeAssistantAPIError → except-Exception
        in ha_get_updates → ENTITY_NOT_FOUND.
        """
        async with MCPAssertions(mcp_client) as mcp:
            data = await mcp.call_tool_failure(
                "ha_get_updates",
                {"entity_id": "update.nonexistent_a2_e2e_xyz_404"},
                expected_error="not found",
            )

            assert data["error"]["code"] == "ENTITY_NOT_FOUND", (
                f"Expected error code ENTITY_NOT_FOUND, got: {data['error']}"
            )
            assert "suggestion" in data["error"], (
                "Error response should include a suggestion"
            )
