"""
E2E Tests for FSM Error Handling and Loading States

Tests error scenarios and loading indicators during FSM transitions:
- Invalid transitions show error toast
- Loading indicator appears during transition
- Network error shows error toast
- Validation error shows user-friendly message
- Confirm dialog appears for dangerous transitions
- Cancel confirm dialog prevents transition

These tests verify:
- Error toast appears with appropriate message
- Loading spinner appears during requests
- Network errors are handled gracefully
- Confirmation dialogs work correctly
- User-friendly error messages are displayed
"""

import re

import pytest
from playwright.sync_api import Page, expect


class TestInvalidTransitionErrors:
    """Tests for error handling when attempting invalid transitions."""

    def test_invalid_transition_shows_error_toast(self, mod_page: Page, live_server, db):
        """Test that attempting an invalid transition shows an error toast."""
        from apps.parks.models import Park

        # Get an operating park
        park = Park.objects.filter(status="OPERATING").first()
        if not park:
            pytest.skip("No operating park available")

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        # Attempt an invalid transition via direct API call
        # For example, trying to reopen an already operating park
        response = mod_page.evaluate(
            f"""
            async () => {{
                const response = await fetch('/core/fsm/parks/park/{park.pk}/transition/transition_to_operating/', {{
                    method: 'POST',
                    headers: {{
                        'HX-Request': 'true',
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
                    }},
                    credentials: 'include'
                }});
                return {{
                    status: response.status,
                    hxTrigger: response.headers.get('HX-Trigger')
                }};
            }}
        """
        )

        # Should return error status (400)
        if response:
            assert response.get("status") in [400, 403]

            # Check for error toast in HX-Trigger header
            hx_trigger = response.get("hxTrigger")
            if hx_trigger:
                assert "showToast" in hx_trigger
                assert "error" in hx_trigger.lower()

    def test_already_transitioned_shows_error(self, mod_page: Page, live_server, db):
        """Test that trying to approve an already-approved submission shows error."""
        from django.contrib.auth import get_user_model
        from django.contrib.contenttypes.models import ContentType

        from apps.moderation.models import EditSubmission
        from apps.parks.models import Park

        User = get_user_model()

        # Create an already-approved submission
        user, _ = User.objects.get_or_create(
            username="testsubmitter2", defaults={"email": "testsubmitter2@example.com"}
        )

        park = Park.objects.first()
        if not park:
            pytest.skip("No park available")

        content_type = ContentType.objects.get_for_model(Park)

        submission = EditSubmission.objects.create(
            user=user,
            content_type=content_type,
            object_id=park.pk,
            submission_type="EDIT",
            changes={"description": "Already approved"},
            reason="Already approved test",
            status="APPROVED",  # Already approved
        )

        try:
            mod_page.goto(f"{live_server.url}/moderation/dashboard/")
            mod_page.wait_for_load_state("networkidle")

            # Try to approve again via direct API call
            response = mod_page.evaluate(
                f"""
                async () => {{
                    const response = await fetch('/core/fsm/moderation/editsubmission/{submission.pk}/transition/transition_to_approved/', {{
                        method: 'POST',
                        headers: {{
                            'HX-Request': 'true',
                            'Content-Type': 'application/x-www-form-urlencoded',
                            'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
                        }},
                        credentials: 'include'
                    }});
                    return {{
                        status: response.status,
                        hxTrigger: response.headers.get('HX-Trigger')
                    }};
                }}
            """
            )

            # Should return error status
            if response:
                assert response.get("status") in [400, 403]

        finally:
            submission.delete()

    def test_nonexistent_transition_shows_error(self, mod_page: Page, live_server, db):
        """Test that requesting a non-existent transition shows error."""
        from apps.parks.models import Park

        park = Park.objects.first()
        if not park:
            pytest.skip("No park available")

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        # Try to call a non-existent transition
        response = mod_page.evaluate(
            f"""
            async () => {{
                const response = await fetch('/core/fsm/parks/park/{park.pk}/transition/nonexistent_transition/', {{
                    method: 'POST',
                    headers: {{
                        'HX-Request': 'true',
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
                    }},
                    credentials: 'include'
                }});
                return {{
                    status: response.status,
                    hxTrigger: response.headers.get('HX-Trigger')
                }};
            }}
        """
        )

        # Should return error status (400 or 404)
        if response:
            assert response.get("status") in [400, 404]


class TestLoadingIndicators:
    """Tests for loading indicator visibility during transitions."""

    def test_loading_indicator_appears_during_transition(self, mod_page: Page, live_server, db):
        """Verify loading spinner appears during HTMX transition."""
        from apps.parks.models import Park

        park = Park.objects.filter(status="OPERATING").first()
        if not park:
            pytest.skip("No operating park available")

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        status_actions = mod_page.locator("[data-park-status-actions]")
        close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")

        if not close_temp_btn.is_visible():
            pytest.skip("Close Temporarily button not visible")

        # Add a route to slow down the request so we can see loading state
        mod_page.route("**/core/fsm/**", lambda route: (mod_page.wait_for_timeout(500), route.continue_()))

        # Handle confirmation dialog
        mod_page.on("dialog", lambda dialog: dialog.accept())

        # Click and immediately check for loading indicator
        close_temp_btn.click()

        # Check for htmx-indicator class or spinner
        loading_indicator = mod_page.locator('.htmx-indicator, .htmx-request .spinner, [class*="loading"]')

        # The loading indicator should appear (may be brief)
        # We wait a short time for it to appear
        try:  # noqa: SIM105
            expect(loading_indicator.first).to_be_visible(timeout=1000)
        except Exception:
            # Loading indicator may have already disappeared if response was fast
            pass

        # Wait for transition to complete
        toast = mod_page.locator("[data-toast]")
        expect(toast).to_be_visible(timeout=5000)

    def test_button_disabled_during_transition(self, mod_page: Page, live_server, db):
        """Test that transition button is disabled during request."""
        from apps.parks.models import Park

        park = Park.objects.filter(status="OPERATING").first()
        if not park:
            pytest.skip("No operating park available")

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        status_actions = mod_page.locator("[data-park-status-actions]")
        close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")

        if not close_temp_btn.is_visible():
            pytest.skip("Close Temporarily button not visible")

        # Add a route to slow down the request
        mod_page.route("**/core/fsm/**", lambda route: (mod_page.wait_for_timeout(1000), route.continue_()))

        mod_page.on("dialog", lambda dialog: dialog.accept())

        # Click button
        close_temp_btn.click()

        # Check if button becomes disabled or has htmx-request class
        expect(close_temp_btn).to_have_attribute("disabled", "", timeout=500)


class TestNetworkErrorHandling:
    """Tests for handling network errors during transitions."""

    def test_network_error_shows_error_toast(self, mod_page: Page, live_server, db):
        """Test that network errors show appropriate error toast."""
        from apps.parks.models import Park

        park = Park.objects.filter(status="OPERATING").first()
        if not park:
            pytest.skip("No operating park available")

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        # Abort network requests to simulate network error
        mod_page.route("**/core/fsm/**", lambda route: route.abort("failed"))

        status_actions = mod_page.locator("[data-park-status-actions]")
        close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")

        if not close_temp_btn.is_visible():
            pytest.skip("Close Temporarily button not visible")

        mod_page.on("dialog", lambda dialog: dialog.accept())

        # Click button - should trigger network error
        close_temp_btn.click()

        # HTMX should show error indication
        # Check for error toast or htmx error event
        error_indicator = mod_page.locator('[data-toast].error, .htmx-error, [class*="error"]')

        # May show as toast or inline error
        try:  # noqa: SIM105
            expect(error_indicator.first).to_be_visible(timeout=5000)
        except Exception:
            # Error may be handled differently
            pass

        # Verify database state was NOT changed
        park.refresh_from_db()
        assert park.status == "OPERATING"

    def test_server_error_shows_user_friendly_message(self, mod_page: Page, live_server, db):
        """Test that server errors show user-friendly messages."""
        from apps.parks.models import Park

        park = Park.objects.filter(status="OPERATING").first()
        if not park:
            pytest.skip("No operating park available")

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        # Return 500 error to simulate server error
        mod_page.route(
            "**/core/fsm/**",
            lambda route: route.fulfill(
                status=500,
                headers={"HX-Trigger": '{"showToast": {"message": "An unexpected error occurred", "type": "error"}}'},
                body="",
            ),
        )

        status_actions = mod_page.locator("[data-park-status-actions]")
        close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")

        if not close_temp_btn.is_visible():
            pytest.skip("Close Temporarily button not visible")

        mod_page.on("dialog", lambda dialog: dialog.accept())

        close_temp_btn.click()

        # Should show user-friendly error message
        toast = mod_page.locator("[data-toast]")
        expect(toast).to_be_visible(timeout=5000)

        # Should not show technical error details to user
        expect(toast).not_to_contain_text("Traceback")
        expect(toast).not_to_contain_text("Exception")


class TestConfirmationDialogs:
    """Tests for confirmation dialogs on dangerous transitions."""

    def test_confirm_dialog_appears_for_reject_transition(self, mod_page: Page, live_server, db):
        """Test that confirmation dialog appears for reject transition."""
        from django.contrib.auth import get_user_model
        from django.contrib.contenttypes.models import ContentType

        from apps.moderation.models import EditSubmission
        from apps.parks.models import Park

        User = get_user_model()

        user, _ = User.objects.get_or_create(
            username="testsubmitter3", defaults={"email": "testsubmitter3@example.com"}
        )

        park = Park.objects.first()
        if not park:
            pytest.skip("No park available")

        content_type = ContentType.objects.get_for_model(Park)

        submission = EditSubmission.objects.create(
            user=user,
            content_type=content_type,
            object_id=park.pk,
            submission_type="EDIT",
            changes={"description": "Confirm dialog test"},
            reason="Confirm dialog test",
            status="PENDING",
        )

        dialog_shown = {"shown": False}

        def handle_dialog(dialog):
            dialog_shown["shown"] = True
            assert "reject" in dialog.message.lower() or "sure" in dialog.message.lower()
            dialog.dismiss()  # Cancel the action

        try:
            mod_page.goto(f"{live_server.url}/moderation/dashboard/")
            mod_page.wait_for_load_state("networkidle")

            mod_page.on("dialog", handle_dialog)

            submission_row = mod_page.locator(f'[data-submission-id="{submission.pk}"]')

            if submission_row.is_visible():
                reject_btn = submission_row.get_by_role("button", name="Reject")
                if reject_btn.is_visible():
                    reject_btn.click()

                    # Give time for dialog to appear
                    mod_page.wait_for_timeout(500)

                    # Verify dialog was shown
                    assert dialog_shown["shown"], "Confirmation dialog should have been shown"

        finally:
            submission.delete()

    def test_cancel_confirm_dialog_prevents_transition(self, mod_page: Page, live_server, db):
        """Test that canceling the confirmation dialog prevents the transition."""
        from django.contrib.auth import get_user_model
        from django.contrib.contenttypes.models import ContentType

        from apps.moderation.models import EditSubmission
        from apps.parks.models import Park

        User = get_user_model()

        user, _ = User.objects.get_or_create(
            username="testsubmitter4", defaults={"email": "testsubmitter4@example.com"}
        )

        park = Park.objects.first()
        if not park:
            pytest.skip("No park available")

        content_type = ContentType.objects.get_for_model(Park)

        submission = EditSubmission.objects.create(
            user=user,
            content_type=content_type,
            object_id=park.pk,
            submission_type="EDIT",
            changes={"description": "Cancel confirm test"},
            reason="Cancel confirm test",
            status="PENDING",
        )

        try:
            mod_page.goto(f"{live_server.url}/moderation/dashboard/")
            mod_page.wait_for_load_state("networkidle")

            # Dismiss (cancel) the dialog
            mod_page.on("dialog", lambda dialog: dialog.dismiss())

            submission_row = mod_page.locator(f'[data-submission-id="{submission.pk}"]')

            if submission_row.is_visible():
                reject_btn = submission_row.get_by_role("button", name="Reject")
                if reject_btn.is_visible():
                    reject_btn.click()

                    # Wait a moment
                    mod_page.wait_for_timeout(500)

                    # Verify submission status was NOT changed
                    submission.refresh_from_db()
                    assert submission.status == "PENDING"

        finally:
            submission.delete()

    def test_accept_confirm_dialog_executes_transition(self, mod_page: Page, live_server, db):
        """Test that accepting the confirmation dialog executes the transition."""
        from django.contrib.auth import get_user_model
        from django.contrib.contenttypes.models import ContentType

        from apps.moderation.models import EditSubmission
        from apps.parks.models import Park

        User = get_user_model()

        user, _ = User.objects.get_or_create(
            username="testsubmitter5", defaults={"email": "testsubmitter5@example.com"}
        )

        park = Park.objects.first()
        if not park:
            pytest.skip("No park available")

        content_type = ContentType.objects.get_for_model(Park)

        submission = EditSubmission.objects.create(
            user=user,
            content_type=content_type,
            object_id=park.pk,
            submission_type="EDIT",
            changes={"description": "Accept confirm test"},
            reason="Accept confirm test",
            status="PENDING",
        )

        try:
            mod_page.goto(f"{live_server.url}/moderation/dashboard/")
            mod_page.wait_for_load_state("networkidle")

            # Accept the dialog
            mod_page.on("dialog", lambda dialog: dialog.accept())

            submission_row = mod_page.locator(f'[data-submission-id="{submission.pk}"]')

            if submission_row.is_visible():
                reject_btn = submission_row.get_by_role("button", name="Reject")
                if reject_btn.is_visible():
                    reject_btn.click()

                    # Wait for transition to complete
                    toast = mod_page.locator("[data-toast]")
                    expect(toast).to_be_visible(timeout=5000)

                    # Verify submission status WAS changed
                    submission.refresh_from_db()
                    assert submission.status == "REJECTED"

        finally:
            submission.delete()


class TestValidationErrors:
    """Tests for validation error handling."""

    def test_validation_error_shows_specific_message(self, mod_page: Page, live_server, db):
        """Test that validation errors show specific error messages."""
        # This test depends on having transitions that require additional data
        # For example, a transition that requires a reason field

        from apps.parks.models import Park

        park = Park.objects.filter(status="OPERATING").first()
        if not park:
            pytest.skip("No operating park available")

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        # Some transitions might require additional data (like closing_date for close_permanently)
        # If validation fails, it should show a specific message

        # This is a placeholder test - actual behavior depends on FSM configuration
        # The test verifies that validation errors are handled gracefully
        pass


class TestToastNotificationBehavior:
    """Tests for toast notification appearance and behavior."""

    def test_success_toast_auto_dismisses(self, mod_page: Page, live_server, db):
        """Test that success toast auto-dismisses after timeout."""
        from apps.parks.models import Park

        park = Park.objects.filter(status="OPERATING").first()
        if not park:
            pytest.skip("No operating park available")

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        status_actions = mod_page.locator("[data-park-status-actions]")
        close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")

        if not close_temp_btn.is_visible():
            pytest.skip("Close Temporarily button not visible")

        mod_page.on("dialog", lambda dialog: dialog.accept())

        close_temp_btn.click()

        # Toast should appear
        toast = mod_page.locator("[data-toast]")
        expect(toast).to_be_visible(timeout=5000)

        # Toast should auto-dismiss after timeout (typically 3-5 seconds)
        # Wait for auto-dismiss
        expect(toast).not_to_be_visible(timeout=10000)

    def test_error_toast_has_correct_styling(self, mod_page: Page, live_server, db):
        """Test that error toast has correct red/danger styling."""
        from apps.parks.models import Park

        park = Park.objects.filter(status="OPERATING").first()
        if not park:
            pytest.skip("No operating park available")

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        # Simulate an error response
        mod_page.route(
            "**/core/fsm/**",
            lambda route: route.fulfill(
                status=400,
                headers={"HX-Trigger": '{"showToast": {"message": "Test error message", "type": "error"}}'},
                body="",
            ),
        )

        status_actions = mod_page.locator("[data-park-status-actions]")
        close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")

        if not close_temp_btn.is_visible():
            pytest.skip("Close Temporarily button not visible")

        mod_page.on("dialog", lambda dialog: dialog.accept())

        close_temp_btn.click()

        # Error toast should appear with error styling
        toast = mod_page.locator("[data-toast]")
        expect(toast).to_be_visible(timeout=5000)

        # Should have error/danger styling (red)
        expect(toast).to_have_class(re.compile(r"error|danger|bg-red|text-red"))

    def test_success_toast_has_correct_styling(self, mod_page: Page, live_server, db):
        """Test that success toast has correct green/success styling."""
        from apps.parks.models import Park

        park = Park.objects.filter(status="OPERATING").first()
        if not park:
            pytest.skip("No operating park available")

        # Reset any previous state
        park.status = "OPERATING"
        park.save()

        mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
        mod_page.wait_for_load_state("networkidle")

        status_actions = mod_page.locator("[data-park-status-actions]")
        close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")

        if not close_temp_btn.is_visible():
            pytest.skip("Close Temporarily button not visible")

        mod_page.on("dialog", lambda dialog: dialog.accept())

        close_temp_btn.click()

        # Success toast should appear with success styling
        toast = mod_page.locator("[data-toast]")
        expect(toast).to_be_visible(timeout=5000)

        # Should have success styling (green)
        expect(toast).to_have_class(re.compile(r"success|bg-green|text-green"))
