"""
FSM Test Helpers

Reusable utility functions for testing FSM transitions:
- Factory functions for creating test objects in specific states
- Assertion helpers for common FSM test scenarios
- Playwright helpers for HTMX swap verification
- Toast notification verification utilities
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any

from django.contrib.auth import get_user_model

if TYPE_CHECKING:
    from apps.moderation.models import EditSubmission, PhotoSubmission
    from apps.parks.models import Park
    from apps.rides.models import Ride
from django.contrib.contenttypes.models import ContentType
from django.db.models import Model
from django.http import HttpResponse

User = get_user_model()


# =============================================================================
# Factory Functions
# =============================================================================


def create_test_submission(
    status: str = "PENDING",
    user: User | None = None,
    park: Model | None = None,
    submission_type: str = "EDIT",
    changes: dict[str, Any] | None = None,
    reason: str = "Test submission",
    **kwargs,
) -> EditSubmission:
    """
    Create a test EditSubmission with the given status.

    Args:
        status: The initial status (PENDING, APPROVED, REJECTED, ESCALATED)
        user: The submitting user (created if not provided)
        park: The park to edit (first park used if not provided)
        submission_type: EDIT or CREATE
        changes: The changes JSON (default: {"description": "Test change"})
        reason: The submission reason
        **kwargs: Additional fields to set on the submission

    Returns:
        EditSubmission instance
    """
    from apps.moderation.models import EditSubmission
    from apps.parks.models import Park

    # Get or create user
    if user is None:
        user, _ = User.objects.get_or_create(
            username="test_submitter", defaults={"email": "test_submitter@example.com"}
        )
        user.set_password("testpass123")
        user.save()

    # Get or create park
    if park is None:
        park = Park.objects.first()
        if not park:
            raise ValueError("No park available for testing")

    # Default changes
    if changes is None:
        changes = {"description": "Test change"}

    content_type = ContentType.objects.get_for_model(Park)

    submission = EditSubmission.objects.create(
        user=user,
        content_type=content_type,
        object_id=park.pk,
        submission_type=submission_type,
        changes=changes,
        reason=reason,
        status=status,
        **kwargs,
    )

    return submission


def create_test_park(status: str = "OPERATING", name: str | None = None, slug: str | None = None, **kwargs) -> Park:
    """
    Create a test Park with the given status.

    Args:
        status: The initial status (OPERATING, CLOSED_TEMP, CLOSED_PERM, DEMOLISHED, RELOCATED)
        name: Park name (auto-generated if not provided)
        slug: Park slug (auto-generated if not provided)
        **kwargs: Additional fields to set on the park

    Returns:
        Park instance
    """
    from tests.factories import ParkFactory

    if name is None:
        import random

        name = f"Test Park {random.randint(1000, 9999)}"

    if slug is None:
        from django.utils.text import slugify

        slug = slugify(name)

    park = ParkFactory(name=name, slug=slug, status=status, **kwargs)

    return park


def create_test_ride(
    status: str = "OPERATING", name: str | None = None, slug: str | None = None, park: Model | None = None, **kwargs
) -> Ride:
    """
    Create a test Ride with the given status.

    Args:
        status: The initial status (OPERATING, CLOSED_TEMP, SBNO, CLOSING, CLOSED_PERM, DEMOLISHED, RELOCATED)
        name: Ride name (auto-generated if not provided)
        slug: Ride slug (auto-generated if not provided)
        park: The park this ride belongs to (created if not provided)
        **kwargs: Additional fields to set on the ride

    Returns:
        Ride instance
    """
    from tests.factories import RideFactory

    if name is None:
        import random

        name = f"Test Ride {random.randint(1000, 9999)}"

    if slug is None:
        from django.utils.text import slugify

        slug = slugify(name)

    ride_kwargs = {"name": name, "slug": slug, "status": status, **kwargs}

    if park is not None:
        ride_kwargs["park"] = park

    ride = RideFactory(**ride_kwargs)

    return ride


def create_test_photo_submission(
    status: str = "PENDING", user: User | None = None, park: Model | None = None, **kwargs
) -> PhotoSubmission:
    """
    Create a test PhotoSubmission with the given status.

    Args:
        status: The initial status (PENDING, APPROVED, REJECTED, ESCALATED)
        user: The submitting user (created if not provided)
        park: The park for the photo (first park used if not provided)
        **kwargs: Additional fields to set on the submission

    Returns:
        PhotoSubmission instance
    """
    from apps.moderation.models import PhotoSubmission
    from apps.parks.models import Park

    # Get or create user
    if user is None:
        user, _ = User.objects.get_or_create(
            username="test_photo_submitter", defaults={"email": "test_photo@example.com"}
        )
        user.set_password("testpass123")
        user.save()

    # Get or create park
    if park is None:
        park = Park.objects.first()
        if not park:
            raise ValueError("No park available for testing")

    content_type = ContentType.objects.get_for_model(Park)

    # Get a photo if available
    try:
        from django_cloudflareimages_toolkit.models import CloudflareImage

        photo = CloudflareImage.objects.first()
        if not photo:
            raise ValueError("No CloudflareImage available for testing")
    except ImportError:
        raise ValueError("CloudflareImage not available") from None

    submission = PhotoSubmission.objects.create(
        user=user,
        content_type=content_type,
        object_id=park.pk,
        photo=photo,
        caption="Test photo submission",
        status=status,
        **kwargs,
    )

    return submission


# =============================================================================
# Assertion Helpers
# =============================================================================


def assert_status_changed(obj: Model, expected_status: str) -> None:
    """
    Assert that an object's status has changed to the expected value.

    Args:
        obj: The model instance to check
        expected_status: The expected status value

    Raises:
        AssertionError: If status doesn't match expected
    """
    obj.refresh_from_db()
    actual_status = getattr(obj, "status", None)
    assert actual_status == expected_status, f"Expected status '{expected_status}', got '{actual_status}'"


def assert_state_log_created(
    obj: Model, transition_name: str, user: User | None = None, expected_state: str | None = None
) -> None:
    """
    Assert that a StateLog entry was created for a transition.

    Args:
        obj: The model instance that was transitioned
        transition_name: The name of the transition (optional, for verification)
        user: The user who performed the transition (optional)
        expected_state: The expected final state in the log

    Raises:
        AssertionError: If no matching StateLog entry found
    """
    from django_fsm_log.models import StateLog

    content_type = ContentType.objects.get_for_model(obj)

    logs = StateLog.objects.filter(content_type=content_type, object_id=obj.pk).order_by("-timestamp")

    assert logs.exists(), "No StateLog entries found for object"

    latest_log = logs.first()

    if expected_state is not None:
        assert latest_log.state == expected_state, f"Expected state '{expected_state}' in log, got '{latest_log.state}'"

    if user is not None:
        assert latest_log.by == user, f"Expected log by user '{user}', got '{latest_log.by}'"


def assert_toast_triggered(response: HttpResponse, message: str | None = None, toast_type: str = "success") -> None:
    """
    Assert that the response contains an HX-Trigger header with toast data.

    Args:
        response: The HTTP response to check
        message: Expected message substring (optional)
        toast_type: Expected toast type ('success', 'error', 'warning', 'info')

    Raises:
        AssertionError: If toast trigger not found or doesn't match
    """
    assert "HX-Trigger" in response, "Response missing HX-Trigger header"

    trigger_data = json.loads(response["HX-Trigger"])
    assert "showToast" in trigger_data, "HX-Trigger missing showToast event"

    toast_data = trigger_data["showToast"]
    assert toast_data.get("type") == toast_type, f"Expected toast type '{toast_type}', got '{toast_data.get('type')}'"

    if message is not None:
        assert message in toast_data.get(
            "message", ""
        ), f"Expected '{message}' in toast message, got '{toast_data.get('message')}'"


def assert_no_status_change(obj: Model, original_status: str) -> None:
    """
    Assert that an object's status has NOT changed from the original.

    Args:
        obj: The model instance to check
        original_status: The original status value

    Raises:
        AssertionError: If status has changed
    """
    obj.refresh_from_db()
    actual_status = getattr(obj, "status", None)
    assert (
        actual_status == original_status
    ), f"Status should not have changed from '{original_status}', but is now '{actual_status}'"


# =============================================================================
# Playwright Helpers
# =============================================================================


def wait_for_htmx_swap(page, target_selector: str, timeout: int = 5000) -> None:
    """
    Wait for an HTMX swap to complete on a target element.

    Args:
        page: Playwright page object
        target_selector: CSS selector for the target element
        timeout: Maximum time to wait in milliseconds
    """
    # Wait for the htmx:afterSwap event
    page.wait_for_function(
        f"""
        () => {{
            const el = document.querySelector('{target_selector}');
            return el && !el.classList.contains('htmx-request');
        }}
        """,
        timeout=timeout,
    )


def verify_transition_buttons_visible(
    page, transitions: list[str], container_selector: str = "[data-status-actions]"
) -> dict[str, bool]:
    """
    Verify which transition buttons are visible on the page.

    Args:
        page: Playwright page object
        transitions: List of transition names to check (e.g., ["Approve", "Reject"])
        container_selector: CSS selector for the container holding the buttons

    Returns:
        Dict mapping transition name to visibility boolean
    """
    container = page.locator(container_selector)
    results = {}

    for transition in transitions:
        button = container.get_by_role("button", name=transition)
        results[transition] = button.is_visible()

    return results


def get_status_badge_text(page, badge_selector: str = "[data-status-badge]") -> str:
    """
    Get the text content of the status badge.

    Args:
        page: Playwright page object
        badge_selector: CSS selector for the status badge

    Returns:
        The text content of the status badge
    """
    badge = page.locator(badge_selector)
    return badge.text_content().strip() if badge.is_visible() else ""


def get_status_badge_class(page, badge_selector: str = "[data-status-badge]") -> str:
    """
    Get the class attribute of the status badge.

    Args:
        page: Playwright page object
        badge_selector: CSS selector for the status badge

    Returns:
        The class attribute of the status badge
    """
    badge = page.locator(badge_selector)
    return badge.get_attribute("class") or ""


def wait_for_toast(page, toast_selector: str = "[data-toast]", timeout: int = 5000):
    """
    Wait for a toast notification to appear.

    Args:
        page: Playwright page object
        toast_selector: CSS selector for the toast element
        timeout: Maximum time to wait in milliseconds

    Returns:
        The toast element locator
    """
    toast = page.locator(toast_selector)
    toast.wait_for(state="visible", timeout=timeout)
    return toast


def wait_for_toast_dismiss(page, toast_selector: str = "[data-toast]", timeout: int = 10000) -> None:
    """
    Wait for a toast notification to be dismissed.

    Args:
        page: Playwright page object
        toast_selector: CSS selector for the toast element
        timeout: Maximum time to wait in milliseconds
    """
    toast = page.locator(toast_selector)
    toast.wait_for(state="hidden", timeout=timeout)


def click_and_confirm(page, button_locator, accept: bool = True) -> None:
    """
    Click a button and handle the confirmation dialog.

    Args:
        page: Playwright page object
        button_locator: The button locator to click
        accept: Whether to accept (True) or dismiss (False) the dialog
    """

    def handle_dialog(dialog):
        if accept:
            dialog.accept()
        else:
            dialog.dismiss()

    page.on("dialog", handle_dialog)
    button_locator.click()


# =============================================================================
# Test Client Helpers
# =============================================================================


def make_htmx_post(client, url: str, data: dict | None = None) -> HttpResponse:
    """
    Make a POST request with HTMX headers.

    Args:
        client: Django test client
        url: The URL to POST to
        data: Optional POST data

    Returns:
        HttpResponse from the request
    """
    return client.post(url, data=data or {}, HTTP_HX_REQUEST="true")


def make_htmx_get(client, url: str) -> HttpResponse:
    """
    Make a GET request with HTMX headers.

    Args:
        client: Django test client
        url: The URL to GET

    Returns:
        HttpResponse from the request
    """
    return client.get(url, HTTP_HX_REQUEST="true")


def get_fsm_transition_url(
    app_label: str, model_name: str, pk: int, transition_name: str, use_slug: bool = False, slug: str | None = None
) -> str:
    """
    Generate the URL for an FSM transition.

    Args:
        app_label: The Django app label (e.g., 'moderation')
        model_name: The model name in lowercase (e.g., 'editsubmission')
        pk: The object's primary key
        transition_name: The name of the transition method
        use_slug: Whether to use slug-based URL
        slug: The object's slug (required if use_slug is True)

    Returns:
        The transition URL string
    """
    from django.urls import reverse

    if use_slug:
        if slug is None:
            raise ValueError("slug is required when use_slug is True")
        return reverse(
            "core:fsm_transition_by_slug",
            kwargs={"app_label": app_label, "model_name": model_name, "slug": slug, "transition_name": transition_name},
        )
    else:
        return reverse(
            "core:fsm_transition",
            kwargs={"app_label": app_label, "model_name": model_name, "pk": pk, "transition_name": transition_name},
        )
