"""Tests for Task 5.9 — Print Engine (HTML → PDF).

Covers:
- PrintTemplate model: creation, str, unique constraint
- render_html_preview: Jinja2 rendering with entity context, CSS injection
- render_pdf: returns non-empty bytes, fails gracefully on template error
- Services: create_print_template, update_print_template, delete_print_template
- Selectors: list_print_templates, get_print_template
- API: GET/POST /print/templates/, GET/PATCH/DELETE /print/templates/{id}/
- API: POST /print/templates/{id}/preview/  → HTML response
- API: unauthenticated access returns 401/403
"""

from __future__ import annotations

import json
from unittest.mock import patch

import pytest
from django.test import Client

from simorgh.apps.platform_core.models import (
    PageOrientation,
    PaperSize,
    PrintTemplate,
)
from simorgh.apps.platform_core.selectors import (
    get_print_template,
    list_print_templates,
)
from simorgh.apps.platform_core.services import (
    PlatformCoreError,
    create_print_template,
    delete_print_template,
    render_html_preview,
    render_pdf,
    update_print_template,
)
from tests.factories import AdminUserFactory, TenantFactory

pytestmark = pytest.mark.django_db


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

SIMPLE_TEMPLATE = "<h1>{{ entity.title }}</h1><p>{{ entity.body }}</p>"
SIMPLE_ENTITY = {"title": "Hello", "body": "World"}


def _make_org_node(tenant):
    from simorgh.apps.organizations.services import create_node

    return create_node(tenant_id=tenant.pk, name="HQ-Print")


def _make_template(tenant, org_node, **kwargs):
    """Create a PrintTemplate with minimal required fields."""
    defaults = dict(
        tenant_id=tenant.pk,
        organization_node_id=org_node.pk,
        entity_type="test.invoice",
        name="Default Invoice",
        html_template=SIMPLE_TEMPLATE,
        css="body { font-family: sans-serif; }",
        paper_size=PaperSize.A4,
        orientation=PageOrientation.PORTRAIT,
    )
    defaults.update(kwargs)
    return create_print_template(**defaults)


# ---------------------------------------------------------------------------
# Model — unit tests
# ---------------------------------------------------------------------------


class TestPrintTemplateModel:
    def test_str_representation(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node, name="My Template", entity_type="crm.order")
        assert "My Template" in str(tmpl)
        assert "crm.order" in str(tmpl)

    def test_default_is_active(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)
        assert tmpl.is_active is True

    def test_default_paper_size_a4(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)
        assert tmpl.paper_size == PaperSize.A4

    def test_default_orientation_portrait(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)
        assert tmpl.orientation == PageOrientation.PORTRAIT

    def test_public_id_is_set(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)
        assert tmpl.public_id is not None

    def test_tenant_scoped(self):
        t1 = TenantFactory()
        t2 = TenantFactory()
        o1 = _make_org_node(t1)
        o2 = _make_org_node(t2)
        t = _make_template(t1, o1, name="Invoice A")
        assert t.tenant_id == t1.pk
        # Same name allowed for different tenant
        t2_tmpl = _make_template(t2, o2, name="Invoice A")
        assert t2_tmpl.pk != t.pk


# ---------------------------------------------------------------------------
# render_html_preview — unit tests
# ---------------------------------------------------------------------------


class TestRenderHtmlPreview:
    def test_renders_entity_variables(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)
        html = render_html_preview(tmpl, entity=SIMPLE_ENTITY)
        assert "Hello" in html
        assert "World" in html

    def test_missing_variable_renders_empty_string(self):
        """Jinja2 Undefined silently returns '' for missing keys."""
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)
        # entity has no 'missing_key'
        html = render_html_preview(tmpl, entity={})
        assert "<h1>" in html  # structure still present

    def test_css_injected(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node, css="p { color: red; }")
        html = render_html_preview(tmpl, entity=SIMPLE_ENTITY)
        assert "p { color: red; }" in html

    def test_page_css_contains_paper_size(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node, paper_size=PaperSize.A5, orientation=PageOrientation.LANDSCAPE)
        html = render_html_preview(tmpl, entity={})
        assert "A5" in html
        assert "landscape" in html

    def test_extra_context_available_in_template(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(
            tenant, org_node,
            html_template="{{ extra_key }}",
        )
        html = render_html_preview(tmpl, entity={}, extra_context={"extra_key": "MAGIC"})
        assert "MAGIC" in html

    def test_returns_full_html_document(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)
        html = render_html_preview(tmpl, entity=SIMPLE_ENTITY)
        assert "<!DOCTYPE html>" in html
        assert "<body>" in html


# ---------------------------------------------------------------------------
# render_pdf — unit tests
# ---------------------------------------------------------------------------


class TestRenderPdf:
    def test_returns_bytes(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)
        result = render_pdf(tmpl, entity=SIMPLE_ENTITY)
        assert isinstance(result, bytes)
        assert len(result) > 0

    def test_pdf_starts_with_pdf_header(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)
        result = render_pdf(tmpl, entity=SIMPLE_ENTITY)
        assert result[:4] == b"%PDF"

    def test_render_pdf_with_extra_context(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node, html_template="<p>{{ note }}</p>")
        result = render_pdf(tmpl, entity={}, extra_context={"note": "Paid"})
        assert isinstance(result, bytes)
        assert len(result) > 0

    def test_render_pdf_raises_on_pisa_error(self):
        tenant = TenantFactory()
        org_node = _make_org_node(tenant)
        tmpl = _make_template(tenant, org_node)

        class FakeResult:
            err = 1

        with patch("xhtml2pdf.pisa.CreatePDF", return_value=FakeResult()):
            with pytest.raises(PlatformCoreError, match="PDF generation failed"):
                render_pdf(tmpl, entity=SIMPLE_ENTITY)


# ---------------------------------------------------------------------------
# Services — unit tests
# ---------------------------------------------------------------------------


class TestPrintTemplateServices:
    def setup_method(self):
        self.tenant = TenantFactory()
        self.org_node = _make_org_node(self.tenant)
        self.user = AdminUserFactory()

    def test_create_template(self):
        tmpl = _make_template(self.tenant, self.org_node)
        assert PrintTemplate.objects.filter(pk=tmpl.pk).exists()

    def test_create_template_sets_created_by(self):
        tmpl = create_print_template(
            tenant_id=self.tenant.pk,
            organization_node_id=self.org_node.pk,
            entity_type="test.receipt",
            name="Receipt",
            html_template="<p>Receipt</p>",
            actor=self.user,
        )
        assert tmpl.created_by_id == self.user.pk

    def test_update_template_name(self):
        tmpl = _make_template(self.tenant, self.org_node)
        updated = update_print_template(tmpl, actor=self.user, name="Updated Name")
        assert updated.name == "Updated Name"

    def test_update_template_is_active(self):
        tmpl = _make_template(self.tenant, self.org_node)
        updated = update_print_template(tmpl, actor=self.user, is_active=False)
        assert updated.is_active is False

    def test_update_template_paper_size(self):
        tmpl = _make_template(self.tenant, self.org_node)
        updated = update_print_template(tmpl, actor=self.user, paper_size=PaperSize.LETTER)
        assert updated.paper_size == PaperSize.LETTER

    def test_delete_template(self):
        tmpl = _make_template(self.tenant, self.org_node)
        pk = tmpl.pk
        delete_print_template(tmpl, actor=self.user)
        assert not PrintTemplate.objects.filter(pk=pk).exists()

    def test_update_ignores_disallowed_fields(self):
        """Fields not in the allowed set should be silently ignored."""
        tmpl = _make_template(self.tenant, self.org_node)
        original_entity_type = tmpl.entity_type
        # entity_type is not in the allowed update set
        updated = update_print_template(tmpl, actor=self.user, entity_type="evil.override")
        assert updated.entity_type == original_entity_type


# ---------------------------------------------------------------------------
# Selectors — unit tests
# ---------------------------------------------------------------------------


class TestPrintTemplateSelectors:
    def setup_method(self):
        self.tenant = TenantFactory()
        self.org_node = _make_org_node(self.tenant)

    def test_list_all_templates(self):
        _make_template(self.tenant, self.org_node, entity_type="et.a", name="T1")
        _make_template(self.tenant, self.org_node, entity_type="et.b", name="T2")
        qs = list_print_templates(self.tenant.pk)
        assert qs.count() == 2

    def test_list_filter_by_entity_type(self):
        _make_template(self.tenant, self.org_node, entity_type="et.a", name="T1")
        _make_template(self.tenant, self.org_node, entity_type="et.b", name="T2")
        qs = list_print_templates(self.tenant.pk, entity_type="et.a")
        assert qs.count() == 1
        assert qs.first().entity_type == "et.a"

    def test_list_filter_is_active(self):
        t1 = _make_template(self.tenant, self.org_node, entity_type="et.c", name="Active")
        update_print_template(t1, is_active=False)  # mark inactive
        _make_template(self.tenant, self.org_node, entity_type="et.c", name="Also Active")
        active_qs = list_print_templates(self.tenant.pk, is_active=True)
        inactive_qs = list_print_templates(self.tenant.pk, is_active=False)
        assert active_qs.count() == 1
        assert inactive_qs.count() == 1

    def test_list_scoped_to_tenant(self):
        other_tenant = TenantFactory()
        other_org = _make_org_node(other_tenant)
        _make_template(self.tenant, self.org_node, entity_type="et.x", name="Mine")
        _make_template(other_tenant, other_org, entity_type="et.x", name="Theirs")
        qs = list_print_templates(self.tenant.pk)
        assert all(t.tenant_id == self.tenant.pk for t in qs)

    def test_get_print_template_found(self):
        tmpl = _make_template(self.tenant, self.org_node)
        result = get_print_template(tmpl.pk, tenant_id=self.tenant.pk)
        assert result is not None
        assert result.pk == tmpl.pk

    def test_get_print_template_not_found(self):
        result = get_print_template(999999, tenant_id=self.tenant.pk)
        assert result is None

    def test_get_print_template_wrong_tenant(self):
        tmpl = _make_template(self.tenant, self.org_node)
        other_tenant = TenantFactory()
        result = get_print_template(tmpl.pk, tenant_id=other_tenant.pk)
        assert result is None


# ---------------------------------------------------------------------------
# API tests
# ---------------------------------------------------------------------------


BASE = "/api/v1"


class TestPrintTemplateAPI:
    def setup_method(self, client):
        self.client = Client()
        self.tenant = TenantFactory()
        self.org_node = _make_org_node(self.tenant)
        self.user = AdminUserFactory()

        from simorgh.apps.iam.models import Role
        from simorgh.apps.memberships.models import Membership

        role = Role.objects.create(tenant=self.tenant, code="admin-print", name="Admin Print")
        mem = Membership.objects.create(
            tenant=self.tenant,
            organization_node=self.org_node,
            role=role,
            status="active",
        )
        mem.users.add(self.user)
        self.client.force_login(self.user)

    def _headers(self):
        return {"HTTP_X_TENANT": self.tenant.slug}

    def _create_payload(self, **overrides):
        payload = {
            "entity_type": "api.invoice",
            "name": "API Invoice Template",
            "html_template": SIMPLE_TEMPLATE,
            "css": "",
            "paper_size": PaperSize.A4,
            "orientation": PageOrientation.PORTRAIT,
        }
        payload.update(overrides)
        return payload

    # ------------------------------------------------------------------
    # GET /print/templates/
    # ------------------------------------------------------------------

    def test_list_templates_empty(self):
        resp = self.client.get(f"{BASE}/print/templates/", **self._headers())
        assert resp.status_code == 200
        assert resp.json() == []

    def test_list_templates_returns_created(self):
        _make_template(self.tenant, self.org_node)
        resp = self.client.get(f"{BASE}/print/templates/", **self._headers())
        assert resp.status_code == 200
        assert len(resp.json()) == 1

    def test_list_templates_filter_entity_type(self):
        _make_template(self.tenant, self.org_node, entity_type="et.1", name="T1")
        _make_template(self.tenant, self.org_node, entity_type="et.2", name="T2")
        resp = self.client.get(
            f"{BASE}/print/templates/?entity_type=et.1", **self._headers()
        )
        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 1
        assert data[0]["entity_type"] == "et.1"

    def test_list_unauthenticated_returns_403(self):
        anon = Client()
        resp = anon.get(f"{BASE}/print/templates/", **self._headers())
        assert resp.status_code in (401, 403)

    # ------------------------------------------------------------------
    # POST /print/templates/
    # ------------------------------------------------------------------

    def test_create_template_returns_201(self):
        resp = self.client.post(
            f"{BASE}/print/templates/",
            data=json.dumps(self._create_payload()),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code == 201
        body = resp.json()
        assert body["name"] == "API Invoice Template"
        assert body["entity_type"] == "api.invoice"

    def test_create_missing_required_field_returns_400(self):
        payload = self._create_payload()
        del payload["name"]
        resp = self.client.post(
            f"{BASE}/print/templates/",
            data=json.dumps(payload),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code == 400

    def test_create_invalid_paper_size_returns_400(self):
        payload = self._create_payload(paper_size="B10")
        resp = self.client.post(
            f"{BASE}/print/templates/",
            data=json.dumps(payload),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code == 400

    def test_create_unauthenticated_returns_403(self):
        anon = Client()
        resp = anon.post(
            f"{BASE}/print/templates/",
            data=json.dumps(self._create_payload()),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code in (401, 403)

    # ------------------------------------------------------------------
    # GET /print/templates/{id}/
    # ------------------------------------------------------------------

    def test_get_template_detail(self):
        tmpl = _make_template(self.tenant, self.org_node)
        resp = self.client.get(
            f"{BASE}/print/templates/{tmpl.pk}/", **self._headers()
        )
        assert resp.status_code == 200
        assert resp.json()["id"] == tmpl.pk

    def test_get_template_detail_not_found(self):
        resp = self.client.get(
            f"{BASE}/print/templates/999999/", **self._headers()
        )
        assert resp.status_code == 404

    def test_get_template_wrong_tenant_returns_404(self):
        other_tenant = TenantFactory()
        other_org = _make_org_node(other_tenant)
        tmpl = _make_template(other_tenant, other_org)
        resp = self.client.get(
            f"{BASE}/print/templates/{tmpl.pk}/", **self._headers()
        )
        assert resp.status_code == 404

    # ------------------------------------------------------------------
    # PATCH /print/templates/{id}/
    # ------------------------------------------------------------------

    def test_patch_template_name(self):
        tmpl = _make_template(self.tenant, self.org_node)
        resp = self.client.patch(
            f"{BASE}/print/templates/{tmpl.pk}/",
            data=json.dumps({"name": "Renamed"}),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code == 200
        assert resp.json()["name"] == "Renamed"

    def test_patch_template_deactivate(self):
        tmpl = _make_template(self.tenant, self.org_node)
        resp = self.client.patch(
            f"{BASE}/print/templates/{tmpl.pk}/",
            data=json.dumps({"is_active": False}),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code == 200
        assert resp.json()["is_active"] is False

    # ------------------------------------------------------------------
    # DELETE /print/templates/{id}/
    # ------------------------------------------------------------------

    def test_delete_template_returns_204(self):
        tmpl = _make_template(self.tenant, self.org_node)
        resp = self.client.delete(
            f"{BASE}/print/templates/{tmpl.pk}/", **self._headers()
        )
        assert resp.status_code == 204
        assert not PrintTemplate.objects.filter(pk=tmpl.pk).exists()

    def test_delete_template_not_found_returns_404(self):
        resp = self.client.delete(
            f"{BASE}/print/templates/999999/", **self._headers()
        )
        assert resp.status_code == 404

    # ------------------------------------------------------------------
    # POST /print/templates/{id}/preview/
    # ------------------------------------------------------------------

    def test_preview_returns_html(self):
        tmpl = _make_template(self.tenant, self.org_node)
        resp = self.client.post(
            f"{BASE}/print/templates/{tmpl.pk}/preview/",
            data=json.dumps({"entity": SIMPLE_ENTITY}),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code == 200
        assert "text/html" in resp["Content-Type"]
        assert "Hello" in resp.content.decode()
        assert "World" in resp.content.decode()

    def test_preview_empty_entity(self):
        tmpl = _make_template(self.tenant, self.org_node)
        resp = self.client.post(
            f"{BASE}/print/templates/{tmpl.pk}/preview/",
            data=json.dumps({}),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code == 200

    def test_preview_not_found_returns_404(self):
        resp = self.client.post(
            f"{BASE}/print/templates/999999/preview/",
            data=json.dumps({"entity": {}}),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code == 404

    def test_preview_unauthenticated_returns_403(self):
        tmpl = _make_template(self.tenant, self.org_node)
        anon = Client()
        resp = anon.post(
            f"{BASE}/print/templates/{tmpl.pk}/preview/",
            data=json.dumps({"entity": {}}),
            content_type="application/json",
            **self._headers(),
        )
        assert resp.status_code in (401, 403)

    def test_list_filter_is_active_false(self):
        t1 = _make_template(self.tenant, self.org_node, entity_type="et.filter", name="Active One")
        update_print_template(t1, is_active=False)
        resp = self.client.get(
            f"{BASE}/print/templates/?is_active=false", **self._headers()
        )
        assert resp.status_code == 200
        data = resp.json()
        assert all(not t["is_active"] for t in data)
