"""Tests for the Automation UI Schema API (task 3.10).

Coverage
--------
- GET /api/v1/automation/trigger-events/       (3.10.1)
- GET /api/v1/automation/condition-fields/{event_name}/  (3.10.2)
- JSON Schema validation for conditions/actions in rule create/update  (3.10.3)
"""

from __future__ import annotations

import uuid

import pytest


# ===========================================================================
# Shared helpers
# ===========================================================================

def _make_user():
    from tests.factories import UserFactory
    return UserFactory()


# ===========================================================================
# Fixtures
# ===========================================================================

@pytest.fixture
def tenant_and_node(db):
    from simorgh.apps.organizations.services import create_node
    from simorgh.apps.tenants.models import Tenant

    tenant = Tenant.objects.create(slug=f"uis-{uuid.uuid4().hex[:8]}", name="UI Schema Tenant")
    node = create_node(tenant_id=tenant.pk, name="Root")
    return tenant, node


@pytest.fixture
def registered_test_event(db):
    """Register a deterministic test event and clean up afterward."""
    from simorgh.apps.events.bus import _REGISTRY, register_event

    spec = register_event(
        "test.ui_schema.event",
        description="Test event for UI schema tests",
        payload_keys=("ticket_id", "status", "priority", "comment_count"),
    )
    yield spec
    # clean up so we don't pollute other tests
    with _REGISTRY.lock:
        _REGISTRY.events.pop("test.ui_schema.event", None)


# ===========================================================================
# 3.10.1 — GET /api/v1/automation/trigger-events/
# ===========================================================================

class TestTriggerEventsEndpoint:
    def test_requires_auth(self, client, tenant_and_node):
        tenant, _ = tenant_and_node
        resp = client.get(
            "/api/v1/automation/trigger-events/",
            HTTP_X_TENANT=tenant.slug,
        )
        assert resp.status_code == 401

    def test_returns_registered_events(self, api_client, tenant_and_node, registered_test_event):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get(
            "/api/v1/automation/trigger-events/",
            HTTP_X_TENANT=tenant.slug,
        )
        assert resp.status_code == 200
        data = resp.json()
        assert "results" in data
        assert "count" in data
        assert data["count"] == len(data["results"])

    def test_event_schema(self, api_client, tenant_and_node, registered_test_event):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get(
            "/api/v1/automation/trigger-events/",
            HTTP_X_TENANT=tenant.slug,
        )
        assert resp.status_code == 200
        results = resp.json()["results"]
        # Find our registered event
        names = {e["name"] for e in results}
        assert "test.ui_schema.event" in names

        evt = next(e for e in results if e["name"] == "test.ui_schema.event")
        assert evt["description"] == "Test event for UI schema tests"
        assert "payload_keys" in evt
        assert "ticket_id" in evt["payload_keys"]
        assert "status" in evt["payload_keys"]

    def test_results_sorted_by_name(self, api_client, tenant_and_node, registered_test_event):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get(
            "/api/v1/automation/trigger-events/",
            HTTP_X_TENANT=tenant.slug,
        )
        assert resp.status_code == 200
        names = [e["name"] for e in resp.json()["results"]]
        assert names == sorted(names)

    def test_requires_tenant_header(self, api_client, db):
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get("/api/v1/automation/trigger-events/")
        assert resp.status_code in (400, 403)


# ===========================================================================
# 3.10.2 — GET /api/v1/automation/condition-fields/{event_name}/
# ===========================================================================

class TestConditionFieldsEndpoint:
    def test_requires_auth(self, client, tenant_and_node):
        tenant, _ = tenant_and_node
        resp = client.get(
            "/api/v1/automation/condition-fields/test.ui_schema.event/",
            HTTP_X_TENANT=tenant.slug,
        )
        assert resp.status_code == 401

    def test_unknown_event_returns_404(self, api_client, tenant_and_node, db):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get(
            "/api/v1/automation/condition-fields/no.such.event/",
            HTTP_X_TENANT=tenant.slug,
        )
        assert resp.status_code == 404

    def test_returns_fields_for_event(self, api_client, tenant_and_node, registered_test_event):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get(
            "/api/v1/automation/condition-fields/test.ui_schema.event/",
            HTTP_X_TENANT=tenant.slug,
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["event_name"] == "test.ui_schema.event"
        assert "fields" in data
        assert "all_operators" in data
        field_keys = {f["key"] for f in data["fields"]}
        assert field_keys == {"ticket_id", "status", "priority", "comment_count"}

    def test_numeric_field_has_numeric_operators(self, api_client, tenant_and_node, registered_test_event):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get(
            "/api/v1/automation/condition-fields/test.ui_schema.event/",
            HTTP_X_TENANT=tenant.slug,
        )
        data = resp.json()
        ticket_id_field = next(f for f in data["fields"] if f["key"] == "ticket_id")
        assert ticket_id_field["type"] == "number"
        ops = {o["op"] for o in ticket_id_field["operators"]}
        # numeric fields should have gt/lt operators
        assert "gt" in ops
        assert "lt" in ops
        # string-only operators should not appear
        assert "contains" not in ops
        assert "startswith" not in ops

    def test_string_field_has_string_operators(self, api_client, tenant_and_node, registered_test_event):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get(
            "/api/v1/automation/condition-fields/test.ui_schema.event/",
            HTTP_X_TENANT=tenant.slug,
        )
        data = resp.json()
        status_field = next(f for f in data["fields"] if f["key"] == "status")
        assert status_field["type"] == "string"
        ops = {o["op"] for o in status_field["operators"]}
        assert "contains" in ops
        assert "startswith" in ops

    def test_count_field_typed_as_number(self, api_client, tenant_and_node, registered_test_event):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get(
            "/api/v1/automation/condition-fields/test.ui_schema.event/",
            HTTP_X_TENANT=tenant.slug,
        )
        data = resp.json()
        count_field = next(f for f in data["fields"] if f["key"] == "comment_count")
        assert count_field["type"] == "number"

    def test_each_operator_has_label(self, api_client, tenant_and_node, registered_test_event):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        resp = api_client.get(
            "/api/v1/automation/condition-fields/test.ui_schema.event/",
            HTTP_X_TENANT=tenant.slug,
        )
        data = resp.json()
        for field in data["fields"]:
            for op in field["operators"]:
                assert "op" in op
                assert "label" in op
                assert op["label"]  # not empty


# ===========================================================================
# 3.10.3 — JSON Schema validation for conditions/actions
# ===========================================================================

class TestConditionsValidation:
    """Conditions validation on rule create (POST) and update (PATCH)."""

    def _patch_perm(self):
        from unittest.mock import patch
        return patch("simorgh.apps.automation.api.views._require_perm")

    @pytest.fixture
    def auth_client(self, api_client, tenant_and_node):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        return api_client, tenant

    def test_valid_conditions_accepted(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Valid Rule",
            "conditions": [{"field": "status", "op": "eq", "value": "open"}],
            "actions": [{"action": "noop.noop", "params": {}}],
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        # 201 or 400 (no org node) — not a validation 400
        assert resp.status_code in (201, 400)
        if resp.status_code == 400:
            assert "conditions" not in resp.json().get("error", "")

    def test_conditions_not_a_list(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Bad Rule",
            "conditions": {"field": "status", "op": "eq"},
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400
        assert "conditions must be a list" in resp.json()["error"]

    def test_condition_missing_field_key(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Bad Rule",
            "conditions": [{"op": "eq", "value": "open"}],
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400
        assert "field" in resp.json()["error"]

    def test_condition_missing_op_key(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Bad Rule",
            "conditions": [{"field": "status", "value": "open"}],
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400
        assert "op" in resp.json()["error"]

    def test_condition_invalid_op(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Bad Rule",
            "conditions": [{"field": "status", "op": "explode", "value": "open"}],
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400
        assert "explode" in resp.json()["error"]

    def test_condition_item_not_dict(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Bad Rule",
            "conditions": ["not-a-dict"],
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400


class TestActionsValidation:
    """Actions validation on rule create (POST) and update (PATCH)."""

    def _patch_perm(self):
        from unittest.mock import patch
        return patch("simorgh.apps.automation.api.views._require_perm")

    @pytest.fixture
    def auth_client(self, api_client, tenant_and_node):
        tenant, _ = tenant_and_node
        user = _make_user()
        api_client.force_login(user)
        return api_client, tenant

    def test_valid_actions_accepted(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Valid Rule",
            "actions": [{"action": "helpdesk.assign_to_user", "params": {"user_id": 1}}],
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code in (201, 400)
        if resp.status_code == 400:
            assert "actions" not in resp.json().get("error", "")

    def test_actions_not_a_list(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Bad Rule",
            "actions": {"action": "noop.noop"},
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400
        assert "actions must be a list" in resp.json()["error"]

    def test_action_missing_action_key(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Bad Rule",
            "actions": [{"params": {}}],
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400
        assert "action" in resp.json()["error"]

    def test_action_params_not_dict(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Bad Rule",
            "actions": [{"action": "noop.noop", "params": "bad"}],
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400
        assert "params" in resp.json()["error"]

    def test_action_item_not_dict(self, auth_client):
        api_client, tenant = auth_client
        payload = {
            "name": "Bad Rule",
            "actions": [42],
        }
        with self._patch_perm():
            resp = api_client.post(
                "/api/v1/automation/rules/",
                payload,
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400

    def test_patch_validates_conditions(self, auth_client, tenant_and_node):
        """PATCH with bad conditions is rejected."""
        from simorgh.apps.automation.models import AutomationRule

        api_client, tenant = auth_client
        _, node = tenant_and_node

        rule = AutomationRule.objects.create(
            tenant=tenant,
            organization_node=node,
            name="Patchable Rule",
            trigger_type="event",
            trigger_event="test.event",
            conditions=[],
            actions=[],
        )
        with self._patch_perm():
            resp = api_client.patch(
                f"/api/v1/automation/rules/{rule.public_id}/",
                {"conditions": "bad"},
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400

    def test_patch_validates_actions(self, auth_client, tenant_and_node):
        """PATCH with bad actions is rejected."""
        from simorgh.apps.automation.models import AutomationRule

        api_client, tenant = auth_client
        _, node = tenant_and_node

        rule = AutomationRule.objects.create(
            tenant=tenant,
            organization_node=node,
            name="Patchable Rule 2",
            trigger_type="event",
            trigger_event="test.event",
            conditions=[],
            actions=[],
        )
        with self._patch_perm():
            resp = api_client.patch(
                f"/api/v1/automation/rules/{rule.public_id}/",
                {"actions": [{"params": {}}]},  # missing "action" key
                format="json",
                HTTP_X_TENANT=tenant.slug,
            )
        assert resp.status_code == 400
