import datetime
import json
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

from django.template.loader import get_template, render_to_string
from django.test import RequestFactory, SimpleTestCase, TestCase

from rr.fields import RoadRunnerField

from .models import (
    BLOG_TEMPLATE_STYLE_DEFAULT,
    BLOG_TEMPLATE_STYLE_PREMIUM,
    BlogPage,
    BlogPageList,
    PageMakerBlogPage,
)
from .utils import paginate_queryset, parse_items_per_page
from .wagtail_blog_coyote_extension.stream_blocks import CoyoteBlogsBlock


def build_queryset_chain(result):
    queryset = MagicMock()
    queryset.live.return_value = queryset
    queryset.filter.return_value = queryset
    queryset.order_by.return_value = result
    queryset.exclude.return_value = result
    return queryset


class BlogPageModelTestCase(TestCase):
    def test_is_creatable(self):
        self.assertTrue(BlogPage.is_creatable)

    def test_get_meta_image_falls_back_to_lead_image(self):
        lead_image = object()
        blog_page = BlogPage(title="Example", slug="example")
        blog_page.__dict__["lead_image"] = lead_image
        blog_page.search_image = None

        self.assertIs(blog_page.get_meta_image(), lead_image)

    def test_get_meta_image_prefers_search_image(self):
        lead_image = object()
        search_image = object()
        blog_page = BlogPage(title="Example", slug="example")
        blog_page.__dict__["lead_image"] = lead_image
        blog_page.search_image = search_image

        self.assertIs(blog_page.get_meta_image(), search_image)

    def test_get_meta_description_prefers_search_description(self):
        blog_page = BlogPage(title="Example", slug="example")
        blog_page.search_description = "SEO summary"
        blog_page.intro = "<p>Intro summary</p>"

        self.assertEqual(blog_page.get_meta_description(), "SEO summary")

    def test_get_meta_description_falls_back_to_stripped_intro(self):
        blog_page = BlogPage(title="Example", slug="example")
        blog_page.search_description = ""
        blog_page.intro = "<p>Intro <strong>summary</strong></p>"

        self.assertEqual(blog_page.get_meta_description(), "Intro summary")

    def test_get_meta_description_returns_none_when_empty(self):
        blog_page = BlogPage(title="Example", slug="example")
        blog_page.search_description = ""
        blog_page.intro = ""

        self.assertIsNone(blog_page.get_meta_description())

    def test_blog_article_structured_data_uses_available_fields(self):
        blog_page = BlogPage(title="Example", slug="example")
        blog_page.seo_title = "Meta title"
        blog_page.search_description = "SEO summary"
        blog_page.post_date = datetime.date(2026, 7, 5)
        blog_page.last_published_at = datetime.datetime(2026, 7, 5, 12, 0, 0)

        with (
            patch.object(
                BlogPage,
                "get_meta_url",
                return_value="https://example.com/blog/example/",
            ),
            patch.object(
                BlogPage,
                "get_meta_image_url",
                return_value="https://example.com/media/example.jpg",
            ),
        ):
            structured_data = blog_page.get_structured_data()

        self.assertEqual(structured_data["@type"], "BlogPosting")
        self.assertEqual(structured_data["headline"], "Meta title")
        self.assertEqual(structured_data["url"], "https://example.com/blog/example/")
        self.assertEqual(structured_data["description"], "SEO summary")
        self.assertEqual(
            structured_data["image"], "https://example.com/media/example.jpg"
        )
        self.assertEqual(structured_data["datePublished"], "2026-07-05")
        self.assertIn("dateModified", structured_data)

    def test_blog_article_structured_data_skips_missing_optional_fields(self):
        blog_page = BlogPage(title="Example", slug="example")
        blog_page.seo_title = ""
        blog_page.search_description = ""
        blog_page.intro = ""
        blog_page.post_date = None
        blog_page.last_published_at = None
        blog_page.latest_revision_created_at = None

        with (
            patch.object(
                BlogPage,
                "get_meta_url",
                return_value="https://example.com/blog/example/",
            ),
            patch.object(BlogPage, "get_meta_image_url", return_value=None),
        ):
            structured_data = blog_page.get_structured_data()

        self.assertEqual(structured_data["@type"], "BlogPosting")
        self.assertNotIn("description", structured_data)
        self.assertNotIn("image", structured_data)
        self.assertNotIn("datePublished", structured_data)
        self.assertNotIn("dateModified", structured_data)


class BlogPageListTestCase(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    def test_is_creatable(self):
        self.assertTrue(BlogPageList.is_creatable)

    def test_parse_items_per_page_uses_default_for_invalid_value(self):
        self.assertEqual(parse_items_per_page("invalid"), 10)
        self.assertEqual(parse_items_per_page(None), 10)
        self.assertEqual(parse_items_per_page(0), 10)
        self.assertEqual(parse_items_per_page(-4), 10)

    def test_parse_items_per_page_accepts_positive_values(self):
        self.assertEqual(parse_items_per_page("5"), 5)
        self.assertEqual(parse_items_per_page(12), 12)

    def test_paginate_queryset_returns_requested_page(self):
        paginator, page_obj = paginate_queryset(
            list(range(9)), page_number=2, per_page=4
        )

        self.assertEqual(paginator.num_pages, 3)
        self.assertEqual(page_obj.number, 2)
        self.assertEqual(list(page_obj.object_list), [4, 5, 6, 7])

    def test_get_context_uses_paginated_blogs(self):
        blog_items = [SimpleNamespace(title=f"Blog {index}") for index in range(12)]
        blog_queryset = build_queryset_chain(blog_items)
        request = self.factory.get("/blog/", {"p": 2})
        page = BlogPageList(title="Blog", slug="blog")

        with (
            patch(
                "ocyan.plugin.wagtail_blog.models.AbstractBasePage.get_context",
                return_value={},
            ),
            patch(
                "ocyan.plugin.wagtail_blog.models.BlogPage.objects.child_of",
                return_value=blog_queryset,
            ),
            patch("ocyan.plugin.wagtail_blog.models.config.get", return_value=5),
            patch.object(BlogPageList, "get_siblings", return_value=[]),
        ):
            context = page.get_context(request)

        self.assertEqual(context["current_page"], 2)
        self.assertEqual(context["last_page"], 3)
        self.assertEqual(len(context["blogs"]), 5)
        self.assertEqual(context["blogs"][0].title, "Blog 5")
        self.assertEqual(context["pages"], context["blogs"])
        self.assertEqual(context["all_blogs"], blog_items)

    def test_get_blog_queryset_filters_by_locale_when_available(self):
        filtered_queryset = MagicMock()
        filtered_queryset.order_by.return_value = filtered_queryset
        base_queryset = MagicMock()
        base_queryset.live.return_value = base_queryset
        base_queryset.filter.return_value = filtered_queryset
        page = BlogPageList(title="Blog", slug="blog")
        page.locale_id = 7

        with patch(
            "ocyan.plugin.wagtail_blog.models.BlogPage.objects.child_of",
            return_value=base_queryset,
        ):
            queryset = page.get_blog_queryset()

        base_queryset.filter.assert_called_once_with(locale_id=7)
        filtered_queryset.order_by.assert_called_once_with("-post_date")
        self.assertIs(queryset, filtered_queryset)

    def test_blog_list_template_loads(self):
        get_template("blog/blog_list.html")
        get_template("blog/packs/premium/blog_list.html")
        get_template("blog/partials/blog_overview_intro.html")
        get_template("blog/partials/blog_overview_card.html")
        get_template("blog/partials/blog_meta.html")
        get_template("blog/partials/blog_structured_data.html")
        get_template("blog/packs/premium/partials/blog_overview_intro.html")
        get_template("blog/packs/premium/partials/blog_overview_card.html")

    def test_template_style_defaults_to_default_pack(self):
        page = BlogPageList(title="Blog", slug="blog")

        self.assertEqual(page.get_template_style(), BLOG_TEMPLATE_STYLE_DEFAULT)
        self.assertEqual(page.get_template(None), "blog/blog_list.html")
        self.assertEqual(page.get_detail_template_name(), "blog/blog_item.html")
        self.assertEqual(
            page.get_pagemaker_detail_template_name(), "blog/pagemaker_blog_item.html"
        )
        self.assertEqual(page.get_stylesheet_paths(), ["wagtail_blog/css/blog.css"])

    def test_template_style_premium_resolves_premium_pack(self):
        page = BlogPageList(
            title="Blog",
            slug="blog",
            template_style=BLOG_TEMPLATE_STYLE_PREMIUM,
        )

        self.assertEqual(page.get_template_style(), BLOG_TEMPLATE_STYLE_PREMIUM)
        self.assertEqual(page.get_template(None), "blog/packs/premium/blog_list.html")
        self.assertEqual(
            page.get_detail_template_name(), "blog/packs/premium/blog_item.html"
        )
        self.assertEqual(
            page.get_pagemaker_detail_template_name(),
            "blog/packs/premium/pagemaker_blog_item.html",
        )
        self.assertEqual(
            page.get_stylesheet_paths(),
            [
                "wagtail_blog/css/blog.css",
                "wagtail_blog/css/blog-premium.css",
            ],
        )

    def test_blog_list_template_renders_empty_state(self):
        with patch(
            "oxyan.themes.templatetags.oxyan.get_oxyan_definition", return_value={}
        ):
            rendered = render_to_string(
                "blog/blog_list.html",
                {
                    "page": SimpleNamespace(title="Blog overview"),
                    "blogs": [],
                    "paginator": SimpleNamespace(num_pages=1),
                },
            )

        self.assertIn("No blog posts have been published yet.", rendered)

    def test_premium_blog_list_template_renders_empty_state(self):
        with patch(
            "oxyan.themes.templatetags.oxyan.get_oxyan_definition", return_value={}
        ):
            rendered = render_to_string(
                "blog/packs/premium/blog_list.html",
                {
                    "page": SimpleNamespace(title="Blog overview"),
                    "blogs": [],
                    "paginator": SimpleNamespace(num_pages=1),
                    "blog_stylesheet_paths": [
                        "wagtail_blog/css/blog.css",
                        "wagtail_blog/css/blog-premium.css",
                    ],
                },
            )

        self.assertIn("No blog posts have been published yet.", rendered)
        self.assertIn("blog-premium.css", rendered)

    def test_blog_overview_card_renders_without_image_or_intro(self):
        rendered = render_to_string(
            "blog/partials/blog_overview_card.html",
            {
                "blog": SimpleNamespace(
                    title="Minimal blog",
                    url="/blog/minimal/",
                    lead_image=None,
                    intro="",
                    post_date=None,
                )
            },
        )

        self.assertIn("Minimal blog", rendered)
        self.assertIn("/blog/minimal/", rendered)

    def test_blog_list_meta_description_uses_search_description(self):
        page = BlogPageList(title="Blog", slug="blog")
        page.search_description = "Overview summary"

        self.assertEqual(page.get_meta_description(), "Overview summary")

    def test_blog_list_structured_data_uses_available_fields(self):
        page = BlogPageList(title="Blog", slug="blog")
        page.seo_title = "Blog overview"
        page.search_description = "Overview summary"

        with patch.object(
            BlogPageList, "get_meta_url", return_value="https://example.com/blog/"
        ):
            structured_data = page.get_structured_data()

        self.assertEqual(structured_data["@type"], "CollectionPage")
        self.assertEqual(structured_data["name"], "Blog overview")
        self.assertEqual(structured_data["url"], "https://example.com/blog/")
        self.assertEqual(structured_data["description"], "Overview summary")

    def test_blog_list_canonical_url_uses_base_url_on_first_page(self):
        page = BlogPageList(title="Blog", slug="blog")
        request = self.factory.get("/blog/")

        with patch.object(
            BlogPageList, "get_meta_url", return_value="https://example.com/blog/"
        ):
            canonical_url = page.get_canonical_url(request)

        self.assertEqual(canonical_url, "https://example.com/blog/")

    def test_blog_list_canonical_url_uses_current_pagination_page(self):
        page = BlogPageList(title="Blog", slug="blog")
        request = self.factory.get("/blog/", {"p": 3})

        with patch.object(
            BlogPageList, "get_meta_url", return_value="https://example.com/blog/"
        ):
            canonical_url = page.get_canonical_url(request)

        self.assertEqual(canonical_url, "https://example.com/blog/?p=3")

    def test_blog_list_prev_next_urls_follow_pagination_contract(self):
        page = BlogPageList(title="Blog", slug="blog")
        request = self.factory.get("/blog/", {"p": 2})
        page_obj = SimpleNamespace(
            number=2,
            has_next=lambda: True,
            previous_page_number=lambda: 1,
            next_page_number=lambda: 3,
        )

        with patch.object(
            BlogPageList, "get_meta_url", return_value="https://example.com/blog/"
        ):
            prev_url = page.get_prev_page_url(request, page_obj)
            next_url = page.get_next_page_url(request, page_obj)

        self.assertEqual(prev_url, "https://example.com/blog/")
        self.assertEqual(next_url, "https://example.com/blog/?p=3")


class BlogDetailTemplateTestCase(TestCase):
    def test_blog_item_template_loads(self):
        get_template("blog/blog_item.html")
        get_template("blog/pagemaker_blog_item.html")
        get_template("blog/packs/premium/blog_item.html")
        get_template("blog/packs/premium/pagemaker_blog_item.html")
        get_template("blog/partials/blog_canonical_links.html")
        get_template("blog/partials/blog_detail_main.html")
        get_template("blog/partials/blog_detail_sidebar.html")
        get_template("blog/packs/premium/partials/blog_detail_main.html")
        get_template("blog/packs/premium/partials/blog_detail_sidebar.html")

    def test_blog_item_template_renders_basic_detail_content(self):
        with patch(
            "oxyan.themes.templatetags.oxyan.get_oxyan_definition", return_value={}
        ):
            rendered = render_to_string(
                "blog/blog_item.html",
                {
                    "page": SimpleNamespace(
                        title="Blog detail",
                        post_date="2026-07-05",
                        intro="<p>Intro</p>",
                        body=[],
                        lead_image=None,
                        get_parent=SimpleNamespace(url="/blog/"),
                    )
                },
            )

        self.assertIn("Back to overview", rendered)
        self.assertIn("Published on", rendered)

    def test_blog_item_template_renders_without_intro_or_recent_posts(self):
        with patch(
            "oxyan.themes.templatetags.oxyan.get_oxyan_definition", return_value={}
        ):
            rendered = render_to_string(
                "blog/blog_item.html",
                {
                    "page": SimpleNamespace(
                        title="Minimal detail",
                        post_date=None,
                        intro="",
                        body=[],
                        lead_image=None,
                        get_parent=SimpleNamespace(url="/blog/"),
                    ),
                    "recent_blogs": [],
                },
            )

        self.assertIn("Recent articles", rendered)
        self.assertIn(
            "More articles will appear here when they are published.", rendered
        )

    def test_premium_blog_item_template_renders_basic_detail_content(self):
        with patch(
            "oxyan.themes.templatetags.oxyan.get_oxyan_definition", return_value={}
        ):
            rendered = render_to_string(
                "blog/packs/premium/blog_item.html",
                {
                    "page": SimpleNamespace(
                        title="Premium detail",
                        post_date="2026-07-05",
                        intro="<p>Intro</p>",
                        body=[],
                        lead_image=None,
                        get_parent=SimpleNamespace(url="/blog/"),
                    ),
                    "recent_blogs": [],
                    "blog_stylesheet_paths": [
                        "wagtail_blog/css/blog.css",
                        "wagtail_blog/css/blog-premium.css",
                    ],
                },
            )

        self.assertIn("Back to overview", rendered)
        self.assertIn("Recent articles", rendered)
        self.assertIn("blog-premium.css", rendered)

    def test_blog_structured_data_partial_renders_json_ld(self):
        structured_payload = json.dumps(
            {"@context": "https://schema.org", "@type": "BlogPosting"}
        )
        rendered = render_to_string(
            "blog/partials/blog_structured_data.html",
            {
                "page": SimpleNamespace(
                    get_structured_data_json=lambda: structured_payload
                )
            },
        )

        self.assertIn('type="application/ld+json"', rendered)
        self.assertIn('"@type": "BlogPosting"', rendered)

    def test_blog_canonical_links_partial_renders_canonical_and_pagination_links(self):
        rendered = render_to_string(
            "blog/partials/blog_canonical_links.html",
            {
                "canonical_url": "https://example.com/blog/?p=2",
                "prev_page_url": "https://example.com/blog/",
                "next_page_url": "https://example.com/blog/?p=3",
            },
        )

        self.assertIn('rel="canonical"', rendered)
        self.assertIn('href="https://example.com/blog/?p=2"', rendered)
        self.assertIn('rel="prev"', rendered)
        self.assertIn('rel="next"', rendered)

    def test_pagemaker_blog_item_template_renders_blog_meta_hooks(self):
        structured_payload = json.dumps(
            {"@context": "https://schema.org", "@type": "BlogPosting"}
        )
        rendered = render_to_string(
            "blog/pagemaker_blog_item.html",
            {
                "page": SimpleNamespace(
                    content=[],
                    get_structured_data_json=lambda: structured_payload,
                ),
                "canonical_url": "https://example.com/blog/example/",
            },
        )

        self.assertIn('rel="canonical"', rendered)
        self.assertIn('type="application/ld+json"', rendered)

    def test_premium_pagemaker_blog_item_template_renders_blog_meta_hooks(self):
        structured_payload = json.dumps(
            {"@context": "https://schema.org", "@type": "BlogPosting"}
        )
        rendered = render_to_string(
            "blog/packs/premium/pagemaker_blog_item.html",
            {
                "page": SimpleNamespace(
                    content=[],
                    get_structured_data_json=lambda: structured_payload,
                ),
                "canonical_url": "https://example.com/blog/example/",
                "blog_stylesheet_paths": [
                    "wagtail_blog/css/blog.css",
                    "wagtail_blog/css/blog-premium.css",
                ],
            },
        )

        self.assertIn('rel="canonical"', rendered)
        self.assertIn('type="application/ld+json"', rendered)
        self.assertIn("blog-premium.css", rendered)


class PageMakerBlogPageTestCase(TestCase):
    def test_pagemaker_blog_page_uses_roadrunner_template(self):
        self.assertEqual(PageMakerBlogPage.template, "blog/pagemaker_blog_item.html")

    def test_pagemaker_blog_page_content_field_is_roadrunner(self):
        self.assertIsInstance(
            PageMakerBlogPage._meta.get_field("content"), RoadRunnerField
        )

    def test_blog_page_inherits_detail_template_from_parent_list(self):
        blog_page = BlogPage(title="Blog detail", slug="blog-detail")
        parent = SimpleNamespace(
            get_specific=lambda: SimpleNamespace(
                get_template_style=lambda: BLOG_TEMPLATE_STYLE_PREMIUM,
                get_detail_template_name=lambda: "blog/packs/premium/blog_item.html",
                get_stylesheet_paths=lambda: [
                    "wagtail_blog/css/blog.css",
                    "wagtail_blog/css/blog-premium.css",
                ],
            )
        )

        with patch.object(BlogPage, "get_parent", return_value=parent):
            self.assertEqual(
                blog_page.get_template_style(), BLOG_TEMPLATE_STYLE_PREMIUM
            )
            self.assertEqual(
                blog_page.get_template(None), "blog/packs/premium/blog_item.html"
            )
            self.assertEqual(
                blog_page.get_stylesheet_paths(),
                [
                    "wagtail_blog/css/blog.css",
                    "wagtail_blog/css/blog-premium.css",
                ],
            )

    def test_pagemaker_blog_page_inherits_premium_template_from_parent_list(self):
        blog_page = PageMakerBlogPage(title="Blog detail", slug="blog-detail")
        parent = SimpleNamespace(
            get_specific=lambda: SimpleNamespace(
                get_pagemaker_detail_template_name=lambda: (
                    "blog/packs/premium/pagemaker_blog_item.html"
                )
            )
        )

        with patch.object(PageMakerBlogPage, "get_parent", return_value=parent):
            self.assertEqual(
                blog_page.get_template(None),
                "blog/packs/premium/pagemaker_blog_item.html",
            )

    def test_premium_stylesheet_exists_in_plugin_static_tree(self):
        css_path = (
            Path(__file__).resolve().parent
            / "static"
            / "wagtail_blog"
            / "css"
            / "blog-premium.css"
        )

        self.assertTrue(css_path.exists())
        self.assertNotIn(
            "text/x-scss",
            css_path.read_text(encoding="utf-8"),
        )

    def test_premium_templates_do_not_use_runtime_scss_loading(self):
        template_dir = (
            Path(__file__).resolve().parent / "templates" / "blog" / "packs" / "premium"
        )

        for template_path in template_dir.rglob("*.html"):
            self.assertNotIn("text/x-scss", template_path.read_text(encoding="utf-8"))


class BlogPageContextTestCase(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    def test_blog_page_context_uses_recent_posts_helper(self):
        sibling_blogs = [SimpleNamespace(title=f"Blog {index}") for index in range(6)]
        sibling_queryset = build_queryset_chain(sibling_blogs)
        recent_queryset = build_queryset_chain(sibling_blogs[1:])
        blog_page = BlogPage(title="Blog detail", slug="blog-detail")
        request = self.factory.get("/blog/example/")

        with (
            patch(
                "ocyan.plugin.wagtail_blog.models.AbstractBasePage.get_context",
                return_value={},
            ),
            patch.object(BlogPage, "get_parent", return_value=SimpleNamespace()),
            patch(
                "ocyan.plugin.wagtail_blog.models.BlogPage.objects.child_of",
                return_value=sibling_queryset,
            ),
            patch.object(
                BlogPage, "get_recent_blog_queryset", return_value=recent_queryset
            ),
        ):
            context = blog_page.get_context(request)

        self.assertEqual(context["blogs"], sibling_blogs)
        self.assertEqual(context["recent_blogs"], sibling_blogs[1:5])

    def test_sibling_blog_queryset_reuses_index_query_contract(self):
        blog_page = BlogPage(title="Blog detail", slug="blog-detail")
        expected_queryset = ["blog-1", "blog-2"]
        blog_index_page = MagicMock()
        blog_index_page.get_blog_queryset.return_value = expected_queryset

        with patch.object(
            BlogPage, "get_blog_index_page", return_value=blog_index_page
        ):
            queryset = blog_page.get_sibling_blog_queryset()

        blog_index_page.get_blog_queryset.assert_called_once_with()
        self.assertEqual(queryset, expected_queryset)


class CoyoteBlogsBlockTestCase(SimpleTestCase):
    def test_coyote_blogs_block_reuses_overview_query_contract(self):
        block = CoyoteBlogsBlock()
        blog_items = [SimpleNamespace(title=f"Blog {index}") for index in range(5)]
        blog_overview_page = MagicMock()
        blog_overview_page.get_blog_queryset.return_value = blog_items
        page_chooser = MagicMock()
        page_chooser.get_specific.return_value = blog_overview_page

        context = block.get_context({"blog_pages": page_chooser})

        blog_overview_page.get_blog_queryset.assert_called_once_with()
        self.assertEqual(len(context["blog_items"]), 3)
        self.assertEqual(context["blog_items"][0].title, "Blog 0")

    def test_coyote_blogs_block_falls_back_to_locale_aware_blog_query(self):
        block = CoyoteBlogsBlock()
        ordered_queryset = ["blog-1", "blog-2"]
        filtered_queryset = MagicMock()
        filtered_queryset.order_by.return_value = ordered_queryset
        base_queryset = MagicMock()
        base_queryset.live.return_value = base_queryset
        base_queryset.filter.return_value = filtered_queryset
        page_chooser = MagicMock()
        blog_overview_page = SimpleNamespace(locale_id=5)
        page_chooser.get_specific.return_value = blog_overview_page

        with patch(
            "ocyan.plugin.wagtail_blog.models.BlogPage.objects.child_of",
            return_value=base_queryset,
        ):
            queryset = block.get_blog_items_queryset({"blog_pages": page_chooser})

        base_queryset.filter.assert_called_once_with(locale_id=5)
        filtered_queryset.order_by.assert_called_once_with("-post_date")
        self.assertEqual(queryset, ordered_queryset)
