from types import SimpleNamespace
from unittest.mock import patch

from django.test import SimpleTestCase, TestCase
from wagtail.models import Locale, Page, PageViewRestriction, Site

from ..constants import (
    SKILL_ADMIN_CONTENT,
    SKILL_BOOKING,
    MODE_ADMIN,
    MODE_CUSTOMER,
    SKILL_CONTACT,
    SKILL_PRODUCT_DETAIL,
    SKILL_PRODUCT_SEARCH,
    SKILL_PAGE_SEARCH,
    SKILL_QUOTE,
    SKILL_SEO_ANALYSIS,
    SKILL_SITE_INFO,
)
from ..services import _build_messages
from ..skills.admin_content import AdminContentSkill
from ..skills.booking import BookingSkill
from ..skills.contact import ContactSkill
from ..skills.page_search import PageSearchSkill
from ..skills.product_detail import ProductDetailSkill
from ..skills.product_search import ProductSearchSkill
from ..skills.quote import QuoteSkill
from ..skills.seo_analysis import SeoAnalysisSkill
from ..skills.registry import get_default_skill_registry
from .utils import (
    FakeCategory,
    FakeAttributeValue,
    FakeProduct,
    FakeSiteCategory,
    build_fake_product_model,
    make_settings,
)


class AiAssistantPageSearchSkillTests(TestCase):
    def setUp(self):
        self.skill = PageSearchSkill()
        self.site = Site.objects.get(is_default_site=True)
        self.root_page = self.site.root_page
        self.page_one = Page(
            title="Shipping Guide",
            slug="shipping-guide",
            search_description="Learn about shipping times and delivery options.",
        )
        self.root_page.add_child(instance=self.page_one)
        self.page_one.save_revision().publish()

        self.page_two = Page(
            title="Shipping FAQ",
            slug="shipping-faq",
            search_description="Frequently asked shipping questions and delivery answers.",
        )
        self.root_page.add_child(instance=self.page_two)
        self.page_two.save_revision().publish()

        self.unpublished_page = Page(
            title="Draft Shipping Plan",
            slug="draft-shipping-plan",
            search_description="Internal draft shipping content.",
        )
        self.unpublished_page.live = False
        self.root_page.add_child(instance=self.unpublished_page)

        self.restricted_page = Page(
            title="Private Shipping Notes",
            slug="private-shipping-notes",
            search_description="Private shipping notes for staff.",
        )
        self.root_page.add_child(instance=self.restricted_page)
        self.restricted_page.save_revision().publish()
        PageViewRestriction.objects.create(
            page=self.restricted_page,
            restriction_type=PageViewRestriction.PASSWORD,
            password="secret",
        )

        global_root = Page.get_first_root_node()
        self.other_root = Page(title="Other Site Home", slug="other-site-home")
        global_root.add_child(instance=self.other_root)
        self.other_root.save_revision().publish()
        self.other_site = Site.objects.create(
            hostname="other.example.test",
            port=80,
            site_name="Other Site",
            root_page=self.other_root,
            is_default_site=False,
        )
        self.other_site_page = Page(
            title="Shipping for Other Site",
            slug="shipping-other-site",
            search_description="Shipping details that belong to another site.",
        )
        self.other_root.add_child(instance=self.other_site_page)
        self.other_site_page.save_revision().publish()

        self.nl_locale, _ = Locale.objects.get_or_create(language_code="nl")
        self.nl_page = Page(
            title="Verzending",
            slug="verzending",
            search_description="Nederlandse verzendinformatie.",
            locale=self.nl_locale,
        )
        self.root_page.add_child(instance=self.nl_page)
        self.nl_page.save_revision().publish()

    def test_page_search_is_registered(self):
        registry = get_default_skill_registry()
        keys = [skill.key for skill in registry.all()]
        self.assertIn(SKILL_PAGE_SEARCH, keys)

    def test_page_search_only_returns_live_public_pages(self):
        results = self.skill._search_pages(
            site=self.site,
            request=SimpleNamespace(LANGUAGE_CODE="en"),
            request_text="shipping delivery",
            max_items=10,
        )
        titles = [result["title"] for result in results]
        self.assertIn("Shipping Guide", titles)
        self.assertIn("Shipping FAQ", titles)
        self.assertNotIn("Draft Shipping Plan", titles)
        self.assertNotIn("Private Shipping Notes", titles)

    def test_page_search_respects_site_boundary(self):
        results = self.skill._search_pages(
            site=self.site,
            request=SimpleNamespace(LANGUAGE_CODE="en"),
            request_text="shipping",
            max_items=10,
        )
        titles = [result["title"] for result in results]
        self.assertNotIn("Shipping for Other Site", titles)

    def test_page_search_respects_locale_when_available(self):
        results = self.skill._search_pages(
            site=self.site,
            request=SimpleNamespace(LANGUAGE_CODE="en"),
            request_text="verzend verzending",
            max_items=10,
        )
        titles = [result["title"] for result in results]
        self.assertNotIn("Verzending", titles)

        dutch_results = self.skill._search_pages(
            site=self.site,
            request=SimpleNamespace(LANGUAGE_CODE="nl"),
            request_text="verzend verzending",
            max_items=10,
        )
        dutch_titles = [result["title"] for result in dutch_results]
        self.assertIn("Verzending", dutch_titles)

    def test_page_search_respects_max_context_items(self):
        results = self.skill._search_pages(
            site=self.site,
            request=SimpleNamespace(LANGUAGE_CODE="en"),
            request_text="shipping",
            max_items=1,
        )
        self.assertEqual(len(results), 1)

    def test_page_search_context_is_included_in_prompt_composition(self):
        settings_obj = make_settings(
            enabled_public_skills=f"{SKILL_SITE_INFO},{SKILL_PAGE_SEARCH}",
            max_skill_context_items=2,
        )
        settings_obj.site = self.site

        messages = _build_messages(
            settings_obj,
            MODE_CUSTOMER,
            [],
            "Tell me about shipping options",
            request=SimpleNamespace(LANGUAGE_CODE="en"),
        )
        rendered = "\n".join(message["content"] for message in messages)
        self.assertIn("Skill context (page_search)", rendered)
        self.assertIn("Relevant public page context", rendered)
        self.assertIn("Shipping Guide", rendered)
        self.assertIn("/shipping-guide/", rendered)

    def test_disabled_page_search_is_not_used(self):
        settings_obj = make_settings(enabled_public_skills=SKILL_SITE_INFO)
        settings_obj.site = self.site

        messages = _build_messages(
            settings_obj,
            MODE_CUSTOMER,
            [],
            "Tell me about shipping options",
            request=SimpleNamespace(LANGUAGE_CODE="en"),
        )
        rendered = "\n".join(message["content"] for message in messages)
        self.assertNotIn("Skill context (page_search)", rendered)


class AiAssistantProductSearchSkillTests(SimpleTestCase):
    def setUp(self):
        self.skill = ProductSearchSkill()
        self.category_coffee = FakeCategory("Coffee", "00010001")
        self.category_tea = FakeCategory("Tea", "00020001")
        self.public_product = FakeProduct(
            title="Coffee Beans Premium",
            slug="coffee-beans-premium",
            description="Fresh roasted espresso beans for home brewing.",
            upc="COF123",
            categories=[self.category_coffee],
            url="/shop/coffee-beans-premium/",
        )
        self.other_site_product = FakeProduct(
            title="Coffee Beans Other Site",
            slug="coffee-beans-other-site",
            description="Coffee beans for another storefront.",
            categories=[FakeCategory("Coffee", "99990001")],
            url="/shop/coffee-beans-other-site/",
        )
        self.private_product = FakeProduct(
            title="Private Wholesale Coffee",
            slug="private-wholesale-coffee",
            description="Not for the public catalogue.",
            is_public=False,
            categories=[self.category_coffee],
        )
        self.variant_product = FakeProduct(
            title="Coffee Beans Premium 1kg",
            slug="coffee-beans-premium-1kg",
            description="Child variant that should not be suggested directly.",
            parent=self.public_product,
            categories=[self.category_coffee],
        )
        self.products = [
            self.public_product,
            self.other_site_product,
            self.private_product,
            self.variant_product,
        ]
        self.product_model = build_fake_product_model(self.products)

    def test_product_search_is_registered(self):
        registry = get_default_skill_registry()
        keys = [skill.key for skill in registry.all()]
        self.assertIn(SKILL_PRODUCT_SEARCH, keys)

    def test_product_search_noops_when_catalogue_is_unavailable(self):
        with patch(
            "ocyan.plugin.ai_assistant.skills.product_search.ProductSearchSkill._load_product_model",
            return_value=None,
        ):
            result = self.skill.build_context(
                "coffee beans",
                None,
                None,
                MODE_CUSTOMER,
                make_settings(enabled_public_skills=SKILL_PRODUCT_SEARCH),
            )

        self.assertEqual(result.items, ())

    def test_product_search_only_returns_public_browsable_products(self):
        with patch.object(
            self.skill, "_load_product_model", return_value=self.product_model
        ), patch.object(
            self.skill,
            "_get_site_category",
            return_value=FakeSiteCategory([self.category_coffee]),
        ):
            results = self.skill._search_products(
                request_text="coffee beans",
                request=SimpleNamespace(),
                max_items=10,
            )

        names = [result["name"] for result in results]
        self.assertIn("Coffee Beans Premium", names)
        self.assertNotIn("Private Wholesale Coffee", names)
        self.assertNotIn("Coffee Beans Premium 1kg", names)
        self.assertNotIn("Coffee Beans Other Site", names)

    def test_product_search_respects_max_context_items(self):
        second_public_product = FakeProduct(
            title="Coffee Grinder",
            slug="coffee-grinder",
            description="Compact grinder for coffee beans.",
            categories=[self.category_coffee],
            url="/shop/coffee-grinder/",
        )
        product_model = build_fake_product_model(
            [self.public_product, second_public_product]
        )
        with patch.object(
            self.skill, "_load_product_model", return_value=product_model
        ), patch.object(
            self.skill,
            "_get_site_category",
            return_value=FakeSiteCategory([self.category_coffee]),
        ):
            results = self.skill._search_products(
                request_text="coffee",
                request=SimpleNamespace(),
                max_items=1,
            )

        self.assertEqual(len(results), 1)

    def test_product_search_context_is_included_in_prompt_composition(self):
        settings_obj = make_settings(
            enabled_public_skills=f"{SKILL_SITE_INFO},{SKILL_PRODUCT_SEARCH}",
            max_skill_context_items=3,
        )
        settings_obj.site = SimpleNamespace(
            site_name="Example Shop", hostname="shop.example.com"
        )
        with patch(
            "ocyan.plugin.ai_assistant.skills.product_search.ProductSearchSkill._load_product_model",
            return_value=self.product_model,
        ), patch(
            "ocyan.plugin.ai_assistant.skills.product_search.ProductSearchSkill._get_site_category",
            return_value=FakeSiteCategory([self.category_coffee]),
        ):
            messages = _build_messages(
                settings_obj,
                MODE_CUSTOMER,
                [],
                "Do you have coffee beans?",
                request=SimpleNamespace(),
            )

        rendered = "\n".join(message["content"] for message in messages)
        self.assertIn("Skill context (product_search)", rendered)
        self.assertIn("Coffee Beans Premium", rendered)
        self.assertIn("/shop/coffee-beans-premium/", rendered)
        self.assertIn("Do not invent prices", rendered)

    def test_disabled_product_search_is_not_used(self):
        settings_obj = make_settings(enabled_public_skills=SKILL_SITE_INFO)
        settings_obj.site = SimpleNamespace(
            site_name="Example Shop", hostname="shop.example.com"
        )
        with patch(
            "ocyan.plugin.ai_assistant.skills.product_search.ProductSearchSkill._load_product_model",
            return_value=self.product_model,
        ):
            messages = _build_messages(
                settings_obj,
                MODE_CUSTOMER,
                [],
                "Do you have coffee beans?",
                request=SimpleNamespace(),
            )

        rendered = "\n".join(message["content"] for message in messages)
        self.assertNotIn("Skill context (product_search)", rendered)

    def test_product_search_does_not_mutate_basket_or_order_state(self):
        with patch.object(
            self.skill, "_load_product_model", return_value=self.product_model
        ), patch.object(
            self.skill,
            "_get_site_category",
            return_value=FakeSiteCategory([self.category_coffee]),
        ):
            result = self.skill.build_context(
                "coffee beans",
                SimpleNamespace(),
                None,
                MODE_CUSTOMER,
                make_settings(enabled_public_skills=SKILL_PRODUCT_SEARCH),
            )

        self.assertTrue(result.items)


class AiAssistantProductDetailSkillTests(SimpleTestCase):
    def setUp(self):
        self.skill = ProductDetailSkill()
        self.category_coffee = FakeCategory("Coffee", "00010001")
        self.category_accessories = FakeCategory("Accessories", "00010002")
        self.parent_product = FakeProduct(
            title="Coffee Machine Pro",
            slug="coffee-machine-pro",
            description="Espresso machine with stainless steel housing.",
            categories=[self.category_coffee],
            url="/shop/coffee-machine-pro/",
            attributes=[
                FakeAttributeValue("Pressure", "15 bar"),
                FakeAttributeValue("Water tank", "2L"),
            ],
        )
        self.variant_product = FakeProduct(
            title="Coffee Machine Pro Black",
            slug="coffee-machine-pro-black",
            description="Black edition of the espresso machine.",
            parent=self.parent_product,
            categories=[self.category_coffee],
            url="/shop/coffee-machine-pro-black/",
        )
        self.parent_product.children = type(self.parent_product.children)(
            [self.variant_product]
        )
        self.accessory_product = FakeProduct(
            title="Coffee Tamper Set",
            slug="coffee-tamper-set",
            description="Accessories for better espresso preparation.",
            categories=[self.category_accessories],
            url="/shop/coffee-tamper-set/",
        )
        self.private_product = FakeProduct(
            title="Wholesale Coffee Machine",
            slug="wholesale-coffee-machine",
            description="Private product not meant for the public catalogue.",
            is_public=False,
            categories=[self.category_coffee],
        )
        self.product_model = build_fake_product_model(
            [
                self.parent_product,
                self.variant_product,
                self.accessory_product,
                self.private_product,
            ]
        )

    def test_product_detail_is_registered(self):
        registry = get_default_skill_registry()
        keys = [skill.key for skill in registry.all()]
        self.assertIn(SKILL_PRODUCT_DETAIL, keys)

    def test_product_detail_noops_when_catalogue_is_unavailable(self):
        with patch(
            "ocyan.plugin.ai_assistant.skills.product_detail.ProductDetailSkill._load_product_model",
            return_value=None,
        ):
            result = self.skill.build_context(
                "Tell me more about the coffee machine",
                None,
                None,
                MODE_CUSTOMER,
                make_settings(enabled_public_skills=SKILL_PRODUCT_DETAIL),
            )

        self.assertEqual(result.items, ())

    def test_product_detail_only_uses_public_browsable_products(self):
        with patch.object(
            self.skill, "_load_product_model", return_value=self.product_model
        ), patch.object(
            self.skill,
            "_get_site_category",
            return_value=FakeSiteCategory([self.category_coffee]),
        ):
            results = self.skill._search_products(
                request_text="Tell me more about the coffee machine",
                request=SimpleNamespace(),
                max_items=10,
            )

        names = [result["name"] for result in results]
        self.assertIn("Coffee Machine Pro", names)
        self.assertNotIn("Wholesale Coffee Machine", names)
        self.assertNotIn("Coffee Machine Pro Black", names)

    def test_product_detail_context_includes_safe_metadata(self):
        with patch.object(
            self.skill, "_load_product_model", return_value=self.product_model
        ), patch.object(
            self.skill,
            "_get_site_category",
            return_value=FakeSiteCategory([self.category_coffee]),
        ):
            result = self.skill.build_context(
                "What are the specifications of the coffee machine?",
                SimpleNamespace(),
                None,
                MODE_CUSTOMER,
                make_settings(enabled_public_skills=SKILL_PRODUCT_DETAIL),
            )

        rendered = "\n".join(result.items)
        self.assertIn("Coffee Machine Pro", rendered)
        self.assertIn("/shop/coffee-machine-pro/", rendered)
        self.assertIn("Category: Coffee", rendered)
        self.assertIn("Specification: Pressure: 15 bar", rendered)
        self.assertIn("Has 1 public variants", rendered)

    def test_product_detail_does_not_mutate_basket_or_order_state(self):
        with patch.object(
            self.skill, "_load_product_model", return_value=self.product_model
        ), patch.object(
            self.skill,
            "_get_site_category",
            return_value=FakeSiteCategory([self.category_coffee]),
        ):
            result = self.skill.build_context(
                "Compare this coffee machine",
                SimpleNamespace(),
                None,
                MODE_CUSTOMER,
                make_settings(enabled_public_skills=SKILL_PRODUCT_DETAIL),
            )

        self.assertTrue(result.items)

    def test_product_detail_context_is_included_in_prompt_composition(self):
        settings_obj = make_settings(
            enabled_public_skills=f"{SKILL_SITE_INFO},{SKILL_PRODUCT_DETAIL}",
            max_skill_context_items=2,
        )
        settings_obj.site = SimpleNamespace(
            site_name="Example Shop", hostname="shop.example.com"
        )
        with patch(
            "ocyan.plugin.ai_assistant.skills.product_detail.ProductDetailSkill._load_product_model",
            return_value=self.product_model,
        ), patch(
            "ocyan.plugin.ai_assistant.skills.product_detail.ProductDetailSkill._get_site_category",
            return_value=FakeSiteCategory([self.category_coffee]),
        ):
            messages = _build_messages(
                settings_obj,
                MODE_CUSTOMER,
                [],
                "Tell me more about the coffee machine",
                request=SimpleNamespace(),
            )

        rendered = "\n".join(message["content"] for message in messages)
        self.assertIn("Skill context (product_detail)", rendered)
        self.assertIn("Coffee Machine Pro", rendered)
        self.assertIn("Do not invent prices", rendered)


class AiAssistantContactSkillTests(SimpleTestCase):
    def setUp(self):
        self.skill = ContactSkill()

    def test_contact_skill_is_registered(self):
        registry = get_default_skill_registry()
        keys = [skill.key for skill in registry.all()]
        self.assertIn(SKILL_CONTACT, keys)

    def test_contact_skill_only_runs_when_enabled(self):
        settings_obj = make_settings(enabled_public_skills=SKILL_SITE_INFO)
        self.assertFalse(
            self.skill.is_enabled(
                SimpleNamespace(site_name="Example", hostname="example.com"),
                settings_obj,
                MODE_CUSTOMER,
            )
        )

    def test_contact_intent_matching_works(self):
        contact_match = self.skill.match(
            "How can I contact you?",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(enabled_public_skills=SKILL_CONTACT),
        )
        quote_match = self.skill.match(
            "I want a quote for a webshop project",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(enabled_public_skills=SKILL_CONTACT),
        )
        self.assertTrue(contact_match.matched)
        self.assertFalse(quote_match.matched)

    def test_public_context_includes_configured_urls(self):
        result = self.skill.build_context(
            "I need help",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_CONTACT,
                contact_page_url="/contact/",
                booking_page_url="/book-a-call/",
                contact_instructions="Share a short project summary only.",
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("/contact/", rendered)
        self.assertIn("/book-a-call/", rendered)
        self.assertIn("Share a short project summary only.", rendered)

    def test_internal_escalation_email_is_not_exposed_publicly(self):
        result = self.skill.build_context(
            "How can I contact support?",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_CONTACT,
                escalation_email="internal@example.com",
                contact_page_url="/contact/",
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("/contact/", rendered)
        self.assertNotIn("internal@example.com", rendered)

    def test_public_support_email_is_only_exposed_when_configured(self):
        without_public_email = self.skill.build_context(
            "How can I contact support?",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_CONTACT,
                contact_page_url="/contact/",
            ),
        )
        self.assertNotIn("Public support email", "\n".join(without_public_email.items))

        with_public_email = self.skill.build_context(
            "How can I contact support?",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_CONTACT,
                public_support_email="support@example.com",
                contact_page_url="/contact/",
            ),
        )
        self.assertIn("support@example.com", "\n".join(with_public_email.items))

    def test_admin_context_may_include_internal_escalation_email(self):
        result = self.skill.build_context(
            "Customer needs human follow-up",
            None,
            None,
            MODE_ADMIN,
            make_settings(
                enabled_admin_skills=SKILL_CONTACT,
                escalation_email="internal@example.com",
                contact_page_url="/contact/",
            ),
        )
        self.assertIn("internal@example.com", "\n".join(result.items))

    def test_prompt_includes_contact_context_when_relevant(self):
        settings_obj = make_settings(
            enabled_public_skills=f"{SKILL_SITE_INFO},{SKILL_CONTACT}",
            contact_page_url="/contact/",
            public_support_email="support@example.com",
            contact_instructions="Share basic project goals, not passwords.",
        )
        settings_obj.site = SimpleNamespace(
            site_name="Example Shop", hostname="shop.example.com"
        )
        messages = _build_messages(
            settings_obj,
            MODE_CUSTOMER,
            [],
            "How can I contact support?",
        )
        rendered = "\n".join(message["content"] for message in messages)
        self.assertIn("Skill context (contact)", rendered)
        self.assertIn("/contact/", rendered)
        self.assertIn("support@example.com", rendered)
        self.assertIn("Do not claim that an email", rendered)


class AiAssistantBookingSkillTests(SimpleTestCase):
    def setUp(self):
        self.skill = BookingSkill()

    def test_booking_skill_is_registered(self):
        registry = get_default_skill_registry()
        keys = [skill.key for skill in registry.all()]
        self.assertIn(SKILL_BOOKING, keys)

    def test_booking_skill_only_runs_when_enabled(self):
        settings_obj = make_settings(enabled_public_skills=SKILL_SITE_INFO)
        self.assertFalse(
            self.skill.is_enabled(
                SimpleNamespace(site_name="Example", hostname="example.com"),
                settings_obj,
                MODE_CUSTOMER,
            )
        )

    def test_booking_intent_matching_works(self):
        for text in (
            "I want to book an appointment",
            "Can I schedule a consultation?",
            "How do I book a demo?",
        ):
            match = self.skill.match(
                text,
                None,
                None,
                MODE_CUSTOMER,
                make_settings(enabled_public_skills=SKILL_BOOKING),
            )
            self.assertTrue(match.matched)

    def test_booking_context_includes_booking_route_when_configured(self):
        result = self.skill.build_context(
            "I need to schedule a consultation",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_BOOKING,
                booking_page_url="/book-a-call/",
                contact_page_url="/contact/",
                contact_instructions="Choose a time and share a short project summary.",
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("/book-a-call/", rendered)
        self.assertIn("/contact/", rendered)
        self.assertIn("Choose a time and share a short project summary.", rendered)

    def test_booking_does_not_expose_internal_escalation_email(self):
        result = self.skill.build_context(
            "I want to book a call",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_BOOKING,
                booking_page_url="/book-a-call/",
                escalation_email="internal@example.com",
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("/book-a-call/", rendered)
        self.assertNotIn("internal@example.com", rendered)

    def test_prompt_includes_booking_context_when_relevant(self):
        settings_obj = make_settings(
            enabled_public_skills=f"{SKILL_SITE_INFO},{SKILL_BOOKING}",
            booking_page_url="/book-a-call/",
            contact_page_url="/contact/",
            contact_instructions="Complete the booking manually on the booking page.",
        )
        settings_obj.site = SimpleNamespace(
            site_name="Example Shop", hostname="shop.example.com"
        )
        messages = _build_messages(
            settings_obj,
            MODE_CUSTOMER,
            [],
            "Can I schedule a demo?",
        )
        rendered = "\n".join(message["content"] for message in messages)
        self.assertIn("Skill context (booking)", rendered)
        self.assertIn("/book-a-call/", rendered)
        self.assertIn("Do not claim that a booking", rendered)
        self.assertIn("complete the booking manually", rendered.lower())


class AiAssistantQuoteSkillTests(SimpleTestCase):
    def setUp(self):
        self.skill = QuoteSkill()

    def test_quote_skill_is_registered(self):
        registry = get_default_skill_registry()
        keys = [skill.key for skill in registry.all()]
        self.assertIn(SKILL_QUOTE, keys)

    def test_quote_skill_only_runs_when_enabled(self):
        settings_obj = make_settings(enabled_public_skills=SKILL_SITE_INFO)
        self.assertFalse(
            self.skill.is_enabled(
                SimpleNamespace(site_name="Example", hostname="example.com"),
                settings_obj,
                MODE_CUSTOMER,
            )
        )

    def test_quote_intent_matching_works(self):
        for text in (
            "I want a quote for a website project",
            "Can I get a price estimate?",
            "I need a proposal for a webshop",
            "I want to start a website project",
        ):
            match = self.skill.match(
                text,
                None,
                None,
                MODE_CUSTOMER,
                make_settings(enabled_public_skills=SKILL_QUOTE),
            )
            self.assertTrue(match.matched)

    def test_quote_context_includes_quote_route_when_configured(self):
        result = self.skill.build_context(
            "I need a quote for a webshop",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_QUOTE,
                quote_page_url="/request-a-quote/",
                contact_page_url="/contact/",
                contact_instructions="Share your business type, goals, and desired features.",
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("/request-a-quote/", rendered)
        self.assertIn("/contact/", rendered)
        self.assertIn(
            "Share your business type, goals, and desired features.", rendered
        )
        self.assertIn("Suggested intake details:", rendered)

    def test_quote_does_not_expose_internal_escalation_email(self):
        result = self.skill.build_context(
            "I want a proposal",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_QUOTE,
                quote_page_url="/request-a-quote/",
                escalation_email="internal@example.com",
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("/request-a-quote/", rendered)
        self.assertNotIn("internal@example.com", rendered)

    def test_prompt_includes_quote_context_when_relevant(self):
        settings_obj = make_settings(
            enabled_public_skills=f"{SKILL_SITE_INFO},{SKILL_QUOTE}",
            quote_page_url="/request-a-quote/",
            contact_page_url="/contact/",
            contact_instructions="Prepare your project goals, existing site URL, and desired features.",
        )
        settings_obj.site = SimpleNamespace(
            site_name="Example Shop", hostname="shop.example.com"
        )
        messages = _build_messages(
            settings_obj,
            MODE_CUSTOMER,
            [],
            "I want a quote for a website",
        )
        rendered = "\n".join(message["content"] for message in messages)
        self.assertIn("Skill context (quote)", rendered)
        self.assertIn("/request-a-quote/", rendered)
        self.assertIn("Do not claim that a quote", rendered)
        self.assertIn("do not invent prices, timelines", rendered.lower())

    def test_contact_skill_no_longer_absorbs_quote_intent(self):
        contact_skill = ContactSkill()
        contact_match = contact_skill.match(
            "I want a quote for a webshop project",
            None,
            None,
            MODE_CUSTOMER,
            make_settings(enabled_public_skills=SKILL_CONTACT),
        )
        self.assertFalse(contact_match.matched)


class AiAssistantServiceRoutingIntegrationTests(TestCase):
    def setUp(self):
        self.site = Site.objects.get(is_default_site=True)
        self.root_page = self.site.root_page

        self.webshop_page = Page(
            title="Webshop Solutions",
            slug="webshop-solutions-integration",
            search_description="Ecommerce and webshop implementation services.",
        )
        self.root_page.add_child(instance=self.webshop_page)
        self.webshop_page.save_revision().publish()

        self.consultation_page = Page(
            title="Consultation Services",
            slug="consultation-services",
            search_description="Book a consultation or demo.",
        )
        self.root_page.add_child(instance=self.consultation_page)
        self.consultation_page.save_revision().publish()

        self.support_page = Page(
            title="Support and Maintenance",
            slug="support-maintenance",
            search_description="Support and maintenance services.",
        )
        self.root_page.add_child(instance=self.support_page)
        self.support_page.save_revision().publish()

    def test_quote_skill_includes_service_specific_route_when_available(self):
        skill = QuoteSkill()
        result = skill.build_context(
            "I need a webshop quote",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_QUOTE,
                quote_page_url="/request-a-quote/",
                contact_page_url="/contact/",
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Detected service category: webshop", rendered)
        self.assertIn("Relevant service URL: /webshop-solutions-integration/", rendered)
        self.assertIn("/request-a-quote/", rendered)

    def test_booking_skill_includes_service_specific_route_when_available(self):
        skill = BookingSkill()
        result = skill.build_context(
            "Book a consultation for a website project",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_BOOKING,
                booking_page_url="/book-a-call/",
                contact_page_url="/contact/",
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Detected service category: booking", rendered)
        self.assertIn("Relevant service URL: /consultation-services/", rendered)
        self.assertIn("/book-a-call/", rendered)

    def test_contact_skill_includes_service_specific_route_when_available(self):
        skill = ContactSkill()
        result = skill.build_context(
            "I need maintenance support",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_CONTACT,
                contact_page_url="/contact/",
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Detected service category: support", rendered)
        self.assertIn("Relevant service URL: /support-maintenance/", rendered)
        self.assertIn("/contact/", rendered)

    def test_fallback_urls_still_work_when_no_specific_page_is_found(self):
        quote_result = QuoteSkill().build_context(
            "I need bespoke legacy platform work",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_CUSTOMER,
            make_settings(
                enabled_public_skills=SKILL_QUOTE,
                quote_page_url="/request-a-quote/",
                contact_page_url="/contact/",
            ),
        )
        rendered = "\n".join(quote_result.items)
        self.assertNotIn("Relevant service URL:", rendered)
        self.assertIn("/request-a-quote/", rendered)
        self.assertIn("/contact/", rendered)


class AiAssistantAdminContentSkillTests(TestCase):
    def setUp(self):
        self.skill = AdminContentSkill()
        self.site = Site.objects.get(is_default_site=True)
        self.root_page = self.site.root_page
        self.contact_page = Page(
            title="Contact",
            slug="contact",
            search_description="Get in touch with the team.",
            seo_title="Contact us",
        )
        self.root_page.add_child(instance=self.contact_page)
        self.contact_page.save_revision().publish()

        self.services_page = Page(
            title="Services",
            slug="services",
            search_description="Overview of available services.",
        )
        self.root_page.add_child(instance=self.services_page)
        self.services_page.save_revision().publish()

        self.draft_page = Page(
            title="Draft Landing Page",
            slug="draft-landing-page",
        )
        self.draft_page.live = False
        self.root_page.add_child(instance=self.draft_page)

    def test_admin_content_is_registered(self):
        registry = get_default_skill_registry()
        keys = [skill.key for skill in registry.all()]
        self.assertIn(SKILL_ADMIN_CONTENT, keys)

    def test_admin_content_is_not_public_safe(self):
        self.assertFalse(self.skill.public_safe)

    def test_admin_content_does_not_run_in_customer_mode(self):
        self.assertFalse(
            self.skill.is_enabled(
                self.site,
                make_settings(enabled_public_skills=SKILL_ADMIN_CONTENT),
                MODE_CUSTOMER,
            )
        )
        result = self.skill.build_context(
            "find pages about services",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_CUSTOMER,
            make_settings(enabled_public_skills=SKILL_ADMIN_CONTENT),
        )
        self.assertEqual(result.items, ())

    def test_admin_content_only_runs_when_enabled_for_admin(self):
        self.assertFalse(
            self.skill.is_enabled(
                self.site,
                make_settings(enabled_admin_skills=SKILL_SITE_INFO),
                MODE_ADMIN,
            )
        )
        self.assertTrue(
            self.skill.is_enabled(
                self.site,
                make_settings(enabled_admin_skills=SKILL_ADMIN_CONTENT),
                MODE_ADMIN,
            )
        )

    def test_admin_content_returns_page_metadata_for_staff_mode(self):
        result = self.skill.build_context(
            "where is the contact page?",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(
                enabled_admin_skills=SKILL_ADMIN_CONTENT,
                max_skill_context_items=5,
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Target page: Contact", rendered)
        self.assertIn("Type:", rendered)
        self.assertIn("Status:", rendered)
        self.assertIn("SEO description:", rendered)

    def test_admin_content_respects_max_context_items(self):
        result = self.skill.build_context(
            "show me pages under the homepage",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(
                enabled_admin_skills=SKILL_ADMIN_CONTENT,
                max_skill_context_items=1,
            ),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Target page: Welcome to your new Wagtail site!", rendered)
        self.assertIn("Children:", rendered)
        self.assertEqual(rendered.count("1. Title:"), 1)

    def test_admin_content_uses_resolver_for_targeted_page_questions(self):
        result = self.skill.build_context(
            "where is the contact page?",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_ADMIN_CONTENT),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Target page: Contact", rendered)
        self.assertNotIn("Matching pages:", rendered)

    def test_admin_content_uses_request_path_for_this_page(self):
        result = self.skill.build_context(
            "what is this page?",
            SimpleNamespace(
                LANGUAGE_CODE="en",
                ai_assistant_source_path="/contact/",
            ),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_ADMIN_CONTENT),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Target page: Contact", rendered)

    def test_admin_content_does_not_expose_body_content(self):
        self.contact_page.search_description = ""
        self.contact_page.save(update_fields=["search_description"])
        result = self.skill.build_context(
            "find pages missing seo description",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_ADMIN_CONTENT),
        )
        rendered = "\n".join(result.items)
        self.assertIn("SEO description: missing", rendered)
        self.assertNotIn("body", rendered.lower())
        self.assertNotIn("Get in touch with the team.", rendered)

    def test_admin_prompt_includes_admin_content_context_only_in_admin_mode(self):
        settings_obj = make_settings(
            enabled_admin_skills=f"{SKILL_SITE_INFO},{SKILL_ADMIN_CONTENT}",
            max_skill_context_items=2,
        )
        settings_obj.site = self.site
        admin_messages = _build_messages(
            settings_obj,
            MODE_ADMIN,
            [],
            "find pages about services",
            request=SimpleNamespace(LANGUAGE_CODE="en"),
        )
        admin_rendered = "\n".join(message["content"] for message in admin_messages)
        self.assertIn("Skill context (admin_content)", admin_rendered)
        self.assertIn("This context is read-only", admin_rendered)

        public_messages = _build_messages(
            settings_obj,
            MODE_CUSTOMER,
            [],
            "find pages about services",
            request=SimpleNamespace(LANGUAGE_CODE="en"),
        )
        public_rendered = "\n".join(message["content"] for message in public_messages)
        self.assertNotIn("Skill context (admin_content)", public_rendered)


class AiAssistantSeoAnalysisSkillTests(TestCase):
    def setUp(self):
        self.skill = SeoAnalysisSkill()
        self.site = Site.objects.get(is_default_site=True)
        self.root_page = self.site.root_page

        self.page_missing_description = Page(
            title="Services",
            slug="services",
            seo_title="Professional services",
            search_description="",
        )
        self.root_page.add_child(instance=self.page_missing_description)
        self.page_missing_description.save_revision().publish()

        self.page_missing_seo_title = Page(
            title="Contact",
            slug="contact",
            seo_title="",
            search_description="How to contact our team for project questions.",
        )
        self.root_page.add_child(instance=self.page_missing_seo_title)
        self.page_missing_seo_title.save_revision().publish()

        self.page_with_metadata = Page(
            title="About",
            slug="about",
            seo_title="About our company and services",
            search_description="Learn about our company, our services, and how we work with clients.",
        )
        self.root_page.add_child(instance=self.page_with_metadata)
        self.page_with_metadata.save_revision().publish()

        global_root = Page.get_first_root_node()
        self.other_root = Page(title="Other Site Home", slug="other-site-home-seo")
        global_root.add_child(instance=self.other_root)
        self.other_root.save_revision().publish()
        self.other_site = Site.objects.create(
            hostname="seo-other.example.test",
            port=80,
            site_name="SEO Other Site",
            root_page=self.other_root,
            is_default_site=False,
        )
        self.other_site_page = Page(
            title="Other Site Contact",
            slug="other-site-contact",
            seo_title="",
            search_description="",
        )
        self.other_root.add_child(instance=self.other_site_page)
        self.other_site_page.save_revision().publish()

    def test_seo_analysis_is_registered(self):
        registry = get_default_skill_registry()
        keys = [skill.key for skill in registry.all()]
        self.assertIn(SKILL_SEO_ANALYSIS, keys)

    def test_seo_analysis_is_not_public_safe(self):
        self.assertFalse(self.skill.public_safe)

    def test_seo_analysis_matches_suggestion_intent(self):
        match = self.skill.match(
            "suggest SEO titles for pages missing SEO title",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_SEO_ANALYSIS),
        )
        self.assertTrue(match.matched)
        self.assertEqual(match.reason, "seo_suggestion_intent")

    def test_seo_analysis_does_not_run_in_customer_mode(self):
        self.assertFalse(
            self.skill.is_enabled(
                self.site,
                make_settings(enabled_public_skills=SKILL_SEO_ANALYSIS),
                MODE_CUSTOMER,
            )
        )
        result = self.skill.build_context(
            "which pages are missing seo descriptions?",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_CUSTOMER,
            make_settings(enabled_public_skills=SKILL_SEO_ANALYSIS),
        )
        self.assertEqual(result.items, ())

    def test_seo_analysis_only_runs_when_enabled_for_admin(self):
        self.assertFalse(
            self.skill.is_enabled(
                self.site,
                make_settings(enabled_admin_skills=SKILL_SITE_INFO),
                MODE_ADMIN,
            )
        )
        self.assertTrue(
            self.skill.is_enabled(
                self.site,
                make_settings(enabled_admin_skills=SKILL_SEO_ANALYSIS),
                MODE_ADMIN,
            )
        )

    def test_seo_analysis_detects_missing_search_description(self):
        result = self.skill.build_context(
            "which pages are missing search descriptions?",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_SEO_ANALYSIS),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Pages missing search descriptions:", rendered)
        self.assertIn("Services", rendered)
        self.assertIn("Search description: missing", rendered)

    def test_seo_analysis_detects_missing_seo_title(self):
        result = self.skill.build_context(
            "find pages with missing seo titles",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_SEO_ANALYSIS),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Pages missing SEO titles:", rendered)
        self.assertIn("Contact", rendered)
        self.assertIn("SEO title: missing", rendered)

    def test_seo_analysis_respects_site_boundary(self):
        result = self.skill.build_context(
            "check seo hygiene for this site",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_SEO_ANALYSIS),
        )
        rendered = "\n".join(result.items)
        self.assertNotIn("Other Site Contact", rendered)

    def test_seo_analysis_respects_max_context_items(self):
        result = self.skill.build_context(
            "check seo hygiene for this site",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(
                enabled_admin_skills=SKILL_SEO_ANALYSIS,
                max_skill_context_items=1,
            ),
        )
        rendered = "\n".join(result.items)
        self.assertEqual(rendered.count("1. Title:"), 1)

    def test_seo_analysis_uses_resolver_for_targeted_page_questions(self):
        result = self.skill.build_context(
            "check SEO for the contact page",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_SEO_ANALYSIS),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Target page SEO analysis:", rendered)
        self.assertIn("Title: Contact", rendered)

    def test_seo_analysis_uses_request_path_for_this_page_suggestions(self):
        result = self.skill.build_context(
            "what should I write for this page's meta description?",
            SimpleNamespace(
                LANGUAGE_CODE="en",
                ai_assistant_source_path="/contact/",
            ),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_SEO_ANALYSIS),
        )
        rendered = "\n".join(result.items)
        self.assertIn("Target page SEO suggestion context:", rendered)
        self.assertIn("Current search description:", rendered)

    def test_admin_prompt_includes_seo_context_only_in_admin_mode(self):
        settings_obj = make_settings(
            enabled_admin_skills=f"{SKILL_SITE_INFO},{SKILL_SEO_ANALYSIS}",
            max_skill_context_items=2,
        )
        settings_obj.site = self.site
        admin_messages = _build_messages(
            settings_obj,
            MODE_ADMIN,
            [],
            "which pages are missing seo descriptions?",
            request=SimpleNamespace(LANGUAGE_CODE="en"),
        )
        admin_rendered = "\n".join(message["content"] for message in admin_messages)
        self.assertIn("Skill context (seo_analysis)", admin_rendered)
        self.assertIn("SEO analysis context may be provided below", admin_rendered)

        public_messages = _build_messages(
            settings_obj,
            MODE_CUSTOMER,
            [],
            "which pages are missing seo descriptions?",
            request=SimpleNamespace(LANGUAGE_CODE="en"),
        )
        public_rendered = "\n".join(message["content"] for message in public_messages)
        self.assertNotIn("Skill context (seo_analysis)", public_rendered)

    def test_admin_prompt_includes_seo_suggestion_instruction_only_in_admin_mode(self):
        settings_obj = make_settings(
            enabled_admin_skills=f"{SKILL_SITE_INFO},{SKILL_SEO_ANALYSIS}",
            max_skill_context_items=2,
        )
        settings_obj.site = self.site
        admin_messages = _build_messages(
            settings_obj,
            MODE_ADMIN,
            [],
            "suggest SEO titles for pages missing SEO title",
            request=SimpleNamespace(LANGUAGE_CODE="en"),
        )
        admin_rendered = "\n".join(message["content"] for message in admin_messages)
        self.assertIn("Skill context (seo_analysis)", admin_rendered)
        self.assertIn("SEO suggestion guidance may be provided below", admin_rendered)
        self.assertIn("Generate suggestions only", admin_rendered)
        self.assertIn("apply changes manually in Wagtail", admin_rendered)

        public_messages = _build_messages(
            settings_obj,
            MODE_CUSTOMER,
            [],
            "suggest SEO titles for pages missing SEO title",
            request=SimpleNamespace(LANGUAGE_CODE="en"),
        )
        public_rendered = "\n".join(message["content"] for message in public_messages)
        self.assertNotIn(
            "SEO suggestion guidance may be provided below", public_rendered
        )

    def test_seo_analysis_suggestion_mode_is_read_only(self):
        original_title = self.page_missing_seo_title.seo_title
        original_description = self.page_missing_description.search_description

        result = self.skill.build_context(
            "what should I write for this page's meta description?",
            SimpleNamespace(LANGUAGE_CODE="en"),
            self.site,
            MODE_ADMIN,
            make_settings(enabled_admin_skills=SKILL_SEO_ANALYSIS),
        )
        rendered = "\n".join(result.items)

        self.page_missing_seo_title.refresh_from_db()
        self.page_missing_description.refresh_from_db()

        self.assertEqual(self.page_missing_seo_title.seo_title, original_title)
        self.assertEqual(
            self.page_missing_description.search_description, original_description
        )
        self.assertIn("Current SEO title:", rendered)
        self.assertIn("Current search description:", rendered)
