"""
Integration tests for the healing pipeline.
Uses a mock Playwright Page to avoid needing a browser in unit tests.
"""

from __future__ import annotations

import asyncio
import time
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from playwright_healer.cache import MemorySelectorCache
from playwright_healer.config import HealerConfig, HealingStrategy, CacheConfig, CacheBackend
from playwright_healer.pipeline import ElementNotFoundError, HealingPipeline


LOGIN_HTML = """<!DOCTYPE html>
<html>
<body>
  <input id="user-name" name="username" type="text" placeholder="Username" aria-label="Username">
  <input id="password" name="password" type="password" placeholder="Password">
  <input id="login-button" type="submit" value="Login" class="btn-submit">
</body>
</html>"""


def _make_mock_page(html: str = LOGIN_HTML, url: str = "https://example.com") -> MagicMock:
    """Create a minimal mock Playwright Page."""
    page = MagicMock()
    page.url = url

    # content() returns the HTML
    page.content = AsyncMock(return_value=html)

    # screenshot returns small PNG stub
    page.screenshot = AsyncMock(return_value=b"\x89PNG\r\n\x1a\n")

    return page


def _make_config(strategy: HealingStrategy = HealingStrategy.HEURISTIC_ONLY) -> HealerConfig:
    """Config with no AI providers — safe for unit tests."""
    return HealerConfig(
        providers=[],
        strategy=strategy,
        enable_heuristic=True,
        enable_dom_fuzzy=True,
        quick_timeout_ms=10,
        element_timeout_ms=100,
        cache=CacheConfig(backend=CacheBackend.MEMORY),
        console_logging=False,
        report_html=False,
        report_json=False,
    )


def _attach_locator_to_page(page: MagicMock, good_selector: str) -> None:
    """Make page.locator(good_selector) return a found element."""
    found_locator = MagicMock()
    found_locator.wait_for = AsyncMock(return_value=None)
    found_locator.count = AsyncMock(return_value=1)
    found_locator.all = AsyncMock(return_value=[found_locator])

    broken_locator = MagicMock()
    broken_locator.wait_for = AsyncMock(side_effect=Exception("Timeout"))
    broken_locator.count = AsyncMock(return_value=0)

    def _locator_factory(selector, *args, **kwargs):
        if selector == good_selector:
            return found_locator
        return broken_locator

    page.locator = MagicMock(side_effect=_locator_factory)
    page.get_by_placeholder = MagicMock(return_value=broken_locator)
    page.get_by_role = MagicMock(return_value=broken_locator)
    page.get_by_text = MagicMock(return_value=broken_locator)
    page.get_by_label = MagicMock(return_value=broken_locator)
    page.get_by_test_id = MagicMock(return_value=broken_locator)


@pytest.mark.asyncio
class TestHeriuticHealing:
    async def test_heals_id_mutation(self):
        """#user-name-wrong → #user-name via heuristic ID mutation."""
        page = _make_mock_page()
        _attach_locator_to_page(page, "#user-name")
        cfg = _make_config(HealingStrategy.HEURISTIC_ONLY)
        pipeline = HealingPipeline(page, cfg)

        # Should find #user-name even though we ask for #user-name-wrong
        # The heuristic tries "#user-name" as a suffix mutation
        locator = await pipeline.find("#user-name-wrong", "Username field")
        assert locator is not None

    async def test_raises_on_total_failure(self):
        """If nothing works, ElementNotFoundError is raised."""
        page = _make_mock_page()
        # All locators fail
        broken_locator = MagicMock()
        broken_locator.wait_for = AsyncMock(side_effect=Exception("Timeout"))
        broken_locator.count = AsyncMock(return_value=0)
        page.locator = MagicMock(return_value=broken_locator)
        page.get_by_placeholder = MagicMock(return_value=broken_locator)
        page.get_by_role = MagicMock(return_value=broken_locator)
        page.get_by_text = MagicMock(return_value=broken_locator)
        page.get_by_label = MagicMock(return_value=broken_locator)
        page.get_by_test_id = MagicMock(return_value=broken_locator)

        cfg = _make_config(HealingStrategy.HEURISTIC_ONLY)
        pipeline = HealingPipeline(page, cfg)

        with pytest.raises(ElementNotFoundError):
            await pipeline.find("#completely-nonexistent", "Nothing here")

    async def test_cache_hit_on_second_call(self):
        """Second call for same broken selector should use cache."""
        page = _make_mock_page()
        _attach_locator_to_page(page, "#user-name")
        cfg = _make_config(HealingStrategy.HEURISTIC_ONLY)
        pipeline = HealingPipeline(page, cfg)

        # First call — heals and caches
        await pipeline.find("#user-name-wrong", "Username field")
        # Second call — should use cache (stage = CACHE)
        await pipeline.find("#user-name-wrong", "Username field")

        events = pipeline.session_report.events
        assert len(events) == 2
        assert events[1].stage == "CACHE"

    async def test_original_selector_returns_immediately(self):
        """Working selector should return without healing."""
        page = _make_mock_page()
        _attach_locator_to_page(page, "#user-name")
        cfg = _make_config(HealingStrategy.SMART)
        pipeline = HealingPipeline(page, cfg)

        await pipeline.find("#user-name", "Username field")
        events = pipeline.session_report.events
        assert events[0].stage == "ORIGINAL"

    async def test_session_report_accumulates_events(self):
        """pipeline.session_report should record all events."""
        page = _make_mock_page()
        _attach_locator_to_page(page, "#user-name")
        cfg = _make_config(HealingStrategy.HEURISTIC_ONLY)
        pipeline = HealingPipeline(page, cfg)

        await pipeline.find("#user-name-wrong", "Username")
        report = pipeline.session_report
        assert report.total >= 1

    async def test_is_present_returns_false_without_raising(self):
        """is_present should return False, not raise, when element not found."""
        page = _make_mock_page()
        broken_locator = MagicMock()
        broken_locator.wait_for = AsyncMock(side_effect=Exception("Timeout"))
        broken_locator.count = AsyncMock(return_value=0)
        page.locator = MagicMock(return_value=broken_locator)
        page.get_by_placeholder = MagicMock(return_value=broken_locator)
        page.get_by_role = MagicMock(return_value=broken_locator)
        page.get_by_text = MagicMock(return_value=broken_locator)
        page.get_by_label = MagicMock(return_value=broken_locator)
        page.get_by_test_id = MagicMock(return_value=broken_locator)

        cfg = _make_config(HealingStrategy.HEURISTIC_ONLY)
        pipeline = HealingPipeline(page, cfg)

        result = await pipeline.is_present("#ghost", "Ghost element")
        assert result is False
