import importlib
import types
from types import SimpleNamespace
from unittest.mock import patch

from django.test import SimpleTestCase, override_settings

from ..capabilities import AssistantCapabilityContext, build_capability_context
from ..constants import (
    CAPABILITY_LEVEL_READ_ONLY,
    MODE_ADMIN,
    MODE_CUSTOMER,
    SKILL_SCOPE_BUSINESS,
    SKILL_SITE_INFO,
)
from ..services import _build_messages, generate_reply
from ..skills.base import AiAssistantSkill, SkillContextResult, SkillMatch
from ..skills.registry import (
    SkillRegistry,
    clear_registered_skills,
    get_default_skill_registry,
    get_registered_external_skills,
    register_skill,
)
from ..skills.site_info import SiteInfoSkill
from .utils import make_settings


class DummyBusinessSkill(AiAssistantSkill):
    key = "dummy_business"
    label = "Dummy business"
    allowed_modes = (MODE_ADMIN,)
    public_safe = False
    skill_scope = SKILL_SCOPE_BUSINESS
    capability_level = CAPABILITY_LEVEL_READ_ONLY

    def is_enabled(self, site, settings_obj, mode: str) -> bool:
        return self.key in settings_obj.enabled_business_skill_keys()

    def match(self, request_text: str, request, site, mode: str, settings_obj):
        return SkillMatch(score=50, reason="dummy_business")

    def build_context(self, request_text: str, request, site, mode: str, settings_obj):
        return SkillContextResult.from_items(
            ["Business-only context item"],
            instruction="Business-only instruction",
            metadata={"skill": self.key},
        )


class AiAssistantServiceTests(SimpleTestCase):
    def setUp(self):
        clear_registered_skills()

    def tearDown(self):
        clear_registered_skills()

    def test_importable(self):
        module = importlib.import_module("ocyan.plugin.ai_assistant.apps")
        self.assertIsNotNone(module)

    def test_generate_reply_missing_api_key_returns_safe_fallback(self):
        text, meta = generate_reply(
            make_settings(openai_api_key=""),
            MODE_CUSTOMER,
            [],
            "Hello",
        )
        self.assertIn("not configured", text)
        self.assertEqual(meta["fallback"], "missing_api_key")
        self.assertEqual(meta["provider"], "openai")

    @override_settings(AI_ASSISTANT_OPENAI_TIMEOUT=33)
    def test_generate_reply_passes_explicit_timeout(self):
        captured = {}

        class FakeCompletions:
            def create(self, **kwargs):
                captured["create"] = kwargs
                return SimpleNamespace(
                    choices=[SimpleNamespace(message=SimpleNamespace(content="Hello"))],
                    usage=SimpleNamespace(total_tokens=17),
                )

        class FakeOpenAI:
            def __init__(self, api_key, timeout):
                captured["api_key"] = api_key
                captured["timeout"] = timeout
                self.chat = SimpleNamespace(completions=FakeCompletions())

        fake_module = types.SimpleNamespace(OpenAI=FakeOpenAI)
        with patch.dict("sys.modules", {"openai": fake_module}):
            text, meta = generate_reply(
                make_settings(openai_api_key="test-key"),
                MODE_CUSTOMER,
                [],
                "Hello",
            )

        self.assertEqual(text, "Hello")
        self.assertEqual(meta["tokens"], 17)
        self.assertEqual(captured["timeout"], 33)
        self.assertEqual(captured["api_key"], "test-key")

    def test_generate_reply_exception_does_not_expose_raw_error(self):
        class FakeOpenAI:
            def __init__(self, api_key, timeout):
                self.chat = SimpleNamespace(completions=self)

            def create(self, **kwargs):
                raise RuntimeError("boom secret-token-123")

        fake_module = types.SimpleNamespace(OpenAI=FakeOpenAI)
        with patch.dict("sys.modules", {"openai": fake_module}):
            text, meta = generate_reply(
                make_settings(openai_api_key="test-key"),
                MODE_CUSTOMER,
                [],
                "Hello",
            )

        self.assertIn("cannot answer", text.lower())
        self.assertEqual(meta["fallback"], "exception")
        self.assertEqual(meta["error_type"], "RuntimeError")
        self.assertNotIn("error", meta)

    @override_settings(
        AI_ASSISTANT_OPENAI_API_KEY="env-first-key",
        OPENAI_API_KEY="fallback-openai-key",
    )
    def test_generate_reply_prefers_env_api_key(self):
        captured = {}

        class FakeCompletions:
            def create(self, **kwargs):
                return SimpleNamespace(
                    choices=[SimpleNamespace(message=SimpleNamespace(content="Hello"))],
                    usage=SimpleNamespace(total_tokens=5),
                )

        class FakeOpenAI:
            def __init__(self, api_key, timeout):
                captured["api_key"] = api_key
                self.chat = SimpleNamespace(completions=FakeCompletions())

        fake_module = types.SimpleNamespace(OpenAI=FakeOpenAI)
        with patch.dict("sys.modules", {"openai": fake_module}):
            generate_reply(
                make_settings(openai_api_key="site-setting-key"),
                MODE_CUSTOMER,
                [],
                "Hello",
            )

        self.assertEqual(captured["api_key"], "env-first-key")

    def test_guardrail_prompt_is_included(self):
        captured = {}

        class FakeCompletions:
            def create(self, **kwargs):
                captured["messages"] = kwargs["messages"]
                return SimpleNamespace(
                    choices=[SimpleNamespace(message=SimpleNamespace(content="Hello"))],
                    usage=SimpleNamespace(total_tokens=5),
                )

        class FakeOpenAI:
            def __init__(self, api_key, timeout):
                self.chat = SimpleNamespace(completions=FakeCompletions())

        fake_module = types.SimpleNamespace(OpenAI=FakeOpenAI)
        with patch.dict("sys.modules", {"openai": fake_module}):
            generate_reply(
                make_settings(openai_api_key="test-key"),
                MODE_CUSTOMER,
                [],
                "Hello",
            )

        self.assertIn(
            "Never ask for or request passwords", captured["messages"][0]["content"]
        )
        self.assertEqual(captured["messages"][0]["role"], "system")

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

    def test_site_info_skill_is_disabled_when_removed_from_public_settings(self):
        skill = SiteInfoSkill()
        settings_obj = make_settings(enabled_public_skills="")
        settings_obj.enabled_skill_keys = lambda mode: []
        self.assertFalse(
            skill.is_enabled(
                SimpleNamespace(site_name="Example", hostname="example.com"),
                settings_obj,
                MODE_CUSTOMER,
            )
        )

    def test_public_and_admin_enabled_skills_are_respected(self):
        site = SimpleNamespace(site_name="Example", hostname="example.com")
        registry = get_default_skill_registry()
        public_settings = make_settings(
            enabled_public_skills=SKILL_SITE_INFO,
            enabled_admin_skills="",
        )
        admin_settings = make_settings(
            enabled_public_skills="",
            enabled_admin_skills=SKILL_SITE_INFO,
        )
        public_keys = [
            skill.key
            for skill in registry.get_enabled_skills(
                site, public_settings, MODE_CUSTOMER
            )
        ]
        admin_keys = [
            skill.key
            for skill in registry.get_enabled_skills(site, admin_settings, MODE_ADMIN)
        ]
        self.assertEqual(public_keys, [SKILL_SITE_INFO])
        self.assertEqual(admin_keys, [SKILL_SITE_INFO])

    def test_business_mode_is_disabled_by_default(self):
        settings_obj = make_settings()
        self.assertFalse(settings_obj.business_assistant_enabled)
        self.assertTrue(settings_obj.business_assistant_requires_staff)
        self.assertEqual(settings_obj.enabled_business_skill_keys(), [])

    def test_business_skill_is_ignored_when_mode_is_off(self):
        site = SimpleNamespace(site_name="Example", hostname="example.com")
        registry = SkillRegistry([SiteInfoSkill(), DummyBusinessSkill()])
        settings_obj = make_settings(
            enabled_admin_skills=SKILL_SITE_INFO,
            business_assistant_enabled=False,
            enabled_business_skills="dummy_business",
        )
        capability_context = AssistantCapabilityContext(
            mode=MODE_ADMIN,
            business_mode_requested=True,
            business_mode_enabled=False,
            staff_allowed=True,
        )
        enabled_keys = [
            skill.key
            for skill in registry.get_enabled_skills(
                site,
                settings_obj,
                MODE_ADMIN,
                capability_context=capability_context,
            )
        ]
        self.assertEqual(enabled_keys, [SKILL_SITE_INFO])

    def test_business_skill_is_ignored_for_customer_mode(self):
        site = SimpleNamespace(site_name="Example", hostname="example.com")
        registry = SkillRegistry([SiteInfoSkill(), DummyBusinessSkill()])
        settings_obj = make_settings(
            enabled_public_skills=SKILL_SITE_INFO,
            business_assistant_enabled=True,
            enabled_business_skills="dummy_business",
        )
        capability_context = AssistantCapabilityContext(
            mode=MODE_CUSTOMER,
            business_mode_requested=True,
            business_mode_enabled=True,
            staff_allowed=True,
        )
        enabled_keys = [
            skill.key
            for skill in registry.get_enabled_skills(
                site,
                settings_obj,
                MODE_CUSTOMER,
                capability_context=capability_context,
            )
        ]
        self.assertEqual(enabled_keys, [SKILL_SITE_INFO])

    def test_business_skill_requires_staff_when_configured(self):
        site = SimpleNamespace(site_name="Example", hostname="example.com")
        registry = SkillRegistry([DummyBusinessSkill()])
        settings_obj = make_settings(
            business_assistant_enabled=True,
            business_assistant_requires_staff=True,
            enabled_business_skills="dummy_business",
        )
        capability_context = AssistantCapabilityContext(
            mode=MODE_ADMIN,
            business_mode_requested=True,
            business_mode_enabled=True,
            staff_allowed=False,
        )
        enabled_keys = [
            skill.key
            for skill in registry.get_enabled_skills(
                site,
                settings_obj,
                MODE_ADMIN,
                capability_context=capability_context,
            )
        ]
        self.assertEqual(enabled_keys, [])

    def test_business_skill_requires_explicit_enabled_list_entry(self):
        site = SimpleNamespace(site_name="Example", hostname="example.com")
        registry = SkillRegistry([DummyBusinessSkill()])
        settings_obj = make_settings(
            business_assistant_enabled=True,
            business_assistant_requires_staff=True,
            enabled_business_skills="",
        )
        capability_context = AssistantCapabilityContext(
            mode=MODE_ADMIN,
            business_mode_requested=True,
            business_mode_enabled=True,
            staff_allowed=True,
        )
        enabled_keys = [
            skill.key
            for skill in registry.get_enabled_skills(
                site,
                settings_obj,
                MODE_ADMIN,
                capability_context=capability_context,
            )
        ]
        self.assertEqual(enabled_keys, [])

    def test_business_skill_context_is_only_included_when_all_gates_pass(self):
        settings_obj = make_settings(
            enabled_admin_skills=SKILL_SITE_INFO,
            business_assistant_enabled=True,
            business_assistant_requires_staff=True,
            business_assistant_instructions="Internal business assistant guidance.",
            enabled_business_skills="dummy_business",
        )
        settings_obj.site = SimpleNamespace(
            site_name="Example Shop", hostname="shop.example.com"
        )
        registry = SkillRegistry([SiteInfoSkill(), DummyBusinessSkill()])

        blocked_messages = _build_messages(
            settings_obj,
            MODE_ADMIN,
            [],
            "Show me business context",
            capability_context=AssistantCapabilityContext(
                mode=MODE_ADMIN,
                business_mode_requested=True,
                business_mode_enabled=True,
                staff_allowed=False,
            ),
            registry=registry,
        )
        blocked_rendered = "\n".join(message["content"] for message in blocked_messages)
        self.assertNotIn("Business-only context item", blocked_rendered)
        self.assertNotIn("Internal business assistant guidance.", blocked_rendered)

        allowed_messages = _build_messages(
            settings_obj,
            MODE_ADMIN,
            [],
            "Show me business context",
            capability_context=AssistantCapabilityContext(
                mode=MODE_ADMIN,
                business_mode_requested=True,
                business_mode_enabled=True,
                staff_allowed=True,
            ),
            registry=registry,
        )
        allowed_rendered = "\n".join(message["content"] for message in allowed_messages)
        self.assertIn("Business-only context item", allowed_rendered)
        self.assertIn("Internal business assistant guidance.", allowed_rendered)
        self.assertIn("Business Assistant Mode is active", allowed_rendered)

    def test_build_capability_context_defaults_to_non_business(self):
        context = build_capability_context(
            mode=MODE_ADMIN,
            settings_obj=make_settings(business_assistant_enabled=True),
            request=SimpleNamespace(user=SimpleNamespace(is_staff=True)),
            business_mode_requested=False,
        )
        self.assertFalse(context.business_skills_allowed)

    def test_registered_external_skill_is_discoverable(self):
        register_skill(DummyBusinessSkill())
        keys = [skill.key for skill in get_default_skill_registry().all()]
        self.assertIn("dummy_business", keys)
        self.assertEqual(
            [skill.key for skill in get_registered_external_skills()],
            ["dummy_business"],
        )

    def test_unenabled_registered_external_skill_is_ignored(self):
        register_skill(DummyBusinessSkill())
        registry = get_default_skill_registry()
        settings_obj = make_settings(
            business_assistant_enabled=True,
            business_assistant_requires_staff=True,
            enabled_business_skills="",
        )
        capability_context = AssistantCapabilityContext(
            mode=MODE_ADMIN,
            business_mode_requested=True,
            business_mode_enabled=True,
            staff_allowed=True,
        )
        enabled_keys = [
            skill.key
            for skill in registry.get_enabled_skills(
                SimpleNamespace(site_name="Example", hostname="example.com"),
                settings_obj,
                MODE_ADMIN,
                capability_context=capability_context,
            )
        ]
        self.assertNotIn("dummy_business", enabled_keys)

    def test_enabled_registered_external_skill_only_works_when_all_business_gates_pass(
        self,
    ):
        register_skill(DummyBusinessSkill())
        settings_obj = make_settings(
            enabled_admin_skills=SKILL_SITE_INFO,
            business_assistant_enabled=True,
            business_assistant_requires_staff=True,
            business_assistant_instructions="Internal business assistant guidance.",
            enabled_business_skills="dummy_business",
        )
        settings_obj.site = SimpleNamespace(
            site_name="Example Shop", hostname="shop.example.com"
        )
        registry = get_default_skill_registry()

        messages = _build_messages(
            settings_obj,
            MODE_ADMIN,
            [],
            "Show me business context",
            capability_context=AssistantCapabilityContext(
                mode=MODE_ADMIN,
                business_mode_requested=True,
                business_mode_enabled=True,
                staff_allowed=True,
            ),
            registry=registry,
        )
        rendered = "\n".join(message["content"] for message in messages)
        self.assertIn("Business-only context item", rendered)

        blocked_messages = _build_messages(
            settings_obj,
            MODE_ADMIN,
            [],
            "Show me business context",
            capability_context=AssistantCapabilityContext(
                mode=MODE_ADMIN,
                business_mode_requested=False,
                business_mode_enabled=True,
                staff_allowed=True,
            ),
            registry=registry,
        )
        blocked_rendered = "\n".join(message["content"] for message in blocked_messages)
        self.assertNotIn("Business-only context item", blocked_rendered)

    def test_clear_registered_skills_keeps_startup_safe(self):
        register_skill(DummyBusinessSkill())
        clear_registered_skills()
        keys = [skill.key for skill in get_default_skill_registry().all()]
        self.assertNotIn("dummy_business", keys)
        self.assertIn(SKILL_SITE_INFO, keys)

    def test_site_info_skill_context_is_safe(self):
        site = SimpleNamespace(site_name="Example Shop", hostname="shop.example.com")
        settings_obj = make_settings(
            business_description="Independent coffee webshop.",
            assistant_tone="Helpful and calm",
            public_assistant_instructions="Answer only from known public site information.",
            openai_api_key="do-not-expose",
            escalation_email="private@example.com",
        )
        result = SiteInfoSkill().build_context(
            "What do you sell?", None, site, MODE_CUSTOMER, settings_obj
        )
        joined = "\n".join(result.items)
        self.assertIn("Example Shop", joined)
        self.assertIn("shop.example.com", joined)
        self.assertIn("Independent coffee webshop.", joined)
        self.assertIn("Helpful and calm", joined)
        self.assertIn("known public site information", joined)
        self.assertNotIn("do-not-expose", joined)
        self.assertNotIn("private@example.com", joined)

    def test_skill_context_is_included_in_prompt_composition(self):
        site = SimpleNamespace(site_name="Example Shop", hostname="shop.example.com")
        settings_obj = make_settings(
            business_description="Independent coffee webshop.",
            public_assistant_instructions="Keep answers grounded in public site content.",
        )
        settings_obj.site = site

        messages = _build_messages(settings_obj, MODE_CUSTOMER, [], "What do you sell?")
        rendered = "\n".join(message["content"] for message in messages)
        self.assertIn("Skill context (site_info)", rendered)
        self.assertIn("Independent coffee webshop.", rendered)
        self.assertIn("Keep answers grounded in public site content.", rendered)
