from __future__ import annotations

from unittest.mock import AsyncMock, patch

from asgiref.sync import async_to_sync
from django.test import TestCase, override_settings
from django.urls import reverse

from core.models import (
    ChatSession,
    ChatTaskSource,
    DerivedTask,
    Message,
    Person,
    PersonIdentifier,
    TaskCompletionPattern,
    TaskProject,
    TaskProviderConfig,
    User,
)
from core.tasks.engine import process_inbound_task_intelligence
from core.views.compose import (
    _command_options_for_channel,
    _toggle_task_announce_for_channel,
)
from core.views.tasks import (
    _apply_safe_defaults_for_user,
    _ensure_default_completion_patterns,
)


class TaskSettingsBackfillTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "defaults-user", "defaults@example.com", "x"
        )
        self.person = Person.objects.create(user=self.user, name="Defaults Person")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="whatsapp",
            identifier="120363402761690215@g.us",
        )
        self.project = TaskProject.objects.create(
            user=self.user,
            name="Legacy Project",
            settings={
                "match_mode": "balanced",
                "require_prefix": False,
                "allowed_prefixes": ["task:", "todo:", "action:"],
                "min_chars": 8,
            },
        )
        self.source = ChatTaskSource.objects.create(
            user=self.user,
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
            project=self.project,
            settings={
                "match_mode": "balanced",
                "require_prefix": False,
                "allowed_prefixes": ["task:", "todo:", "action:"],
                "min_chars": 8,
            },
            enabled=True,
        )

    def test_backfill_applies_safe_defaults_for_legacy_rows(self):
        _apply_safe_defaults_for_user(self.user)
        self.project.refresh_from_db()
        self.source.refresh_from_db()
        self.assertEqual("strict", self.project.settings.get("match_mode"))
        self.assertTrue(bool(self.project.settings.get("require_prefix")))
        self.assertEqual(
            ["task:", "todo:"], self.project.settings.get("allowed_prefixes")
        )
        self.assertFalse(bool(self.project.settings.get("announce_task_id")))
        self.assertEqual("strict", self.source.settings.get("match_mode"))
        self.assertTrue(bool(self.source.settings.get("require_prefix")))

    def test_default_completion_phrases_seeded(self):
        _ensure_default_completion_patterns(self.user)
        phrases = set(
            TaskCompletionPattern.objects.filter(user=self.user).values_list(
                "phrase", flat=True
            )
        )
        self.assertTrue({"done", "completed", "fixed"}.issubset(phrases))


class TaskAnnounceToggleTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user("toggle-user", "toggle@example.com", "x")
        self.person = Person.objects.create(user=self.user, name="Toggle Person")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="whatsapp",
            identifier="120363402761690215@g.us",
        )
        self.project = TaskProject.objects.create(user=self.user, name="Toggle Project")
        self.source = ChatTaskSource.objects.create(
            user=self.user,
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
            project=self.project,
            settings={"announce_task_id": False},
            enabled=True,
        )

    def test_toggle_task_announce_updates_source_settings(self):
        ok, err = _toggle_task_announce_for_channel(
            user=self.user,
            service="whatsapp",
            identifier="120363402761690215",
            enabled=True,
        )
        self.assertTrue(ok)
        self.assertEqual("", err)
        self.source.refresh_from_db()
        self.assertTrue(bool(self.source.settings.get("announce_task_id")))

    def test_command_options_include_bp_subcommands(self):
        options = _command_options_for_channel(
            self.user,
            "whatsapp",
            "120363402761690215",
        )
        names = [str(row.get("name") or "").strip().lower() for row in options]
        self.assertIn("bp", names)
        self.assertIn("bp set", names)
        self.assertIn("bp set range", names)
        self.assertEqual(
            {"bp", "bp set", "bp set range"},
            {name for name in names if name.startswith("bp")},
        )


@override_settings(TASK_DERIVATION_USE_AI=False)
class TaskAnnounceRuntimeTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user("runtime-user", "runtime@example.com", "x")
        self.person = Person.objects.create(user=self.user, name="Runtime Person")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="whatsapp",
            identifier="120363402761690215@g.us",
        )
        self.session = ChatSession.objects.create(
            user=self.user, identifier=self.identifier
        )
        self.project = TaskProject.objects.create(
            user=self.user, name="Runtime Project"
        )

    def _seed_source(self, announce_enabled: bool):
        return ChatTaskSource.objects.create(
            user=self.user,
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
            project=self.project,
            settings={
                "match_mode": "strict",
                "require_prefix": True,
                "allowed_prefixes": ["task:", "todo:"],
                "announce_task_id": announce_enabled,
            },
            enabled=True,
        )

    def _msg(self, text: str, ts: int = 1000):
        return Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text=text,
            ts=ts,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )

    def test_no_announce_send_when_disabled(self):
        self._seed_source(False)
        with patch(
            "core.tasks.engine.send_message_raw", new=AsyncMock()
        ) as mocked_send:
            async_to_sync(process_inbound_task_intelligence)(
                self._msg("task: rotate secrets")
            )
        self.assertTrue(DerivedTask.objects.exists())
        mocked_send.assert_not_awaited()

    def test_announce_send_when_enabled(self):
        self._seed_source(True)
        with patch(
            "core.tasks.engine.send_message_raw", new=AsyncMock(return_value=True)
        ) as mocked_send:
            async_to_sync(process_inbound_task_intelligence)(
                self._msg("task: rotate secrets")
            )
        self.assertTrue(DerivedTask.objects.exists())
        mocked_send.assert_awaited()


class TaskSettingsViewActionsTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "task-settings-user", "ts@example.com", "x"
        )
        self.client.force_login(self.user)
        self.project = TaskProject.objects.create(user=self.user, name="Project A")
        self.source = ChatTaskSource.objects.create(
            user=self.user,
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
            project=self.project,
            settings={"match_mode": "strict"},
            enabled=True,
        )

    def test_source_delete_removes_mapping(self):
        response = self.client.post(
            reverse("tasks_settings"),
            {
                "action": "source_delete",
                "source_id": str(self.source.id),
                "prefill_service": "whatsapp",
                "prefill_identifier": "120363402761690215@g.us",
            },
            follow=True,
        )
        self.assertEqual(200, response.status_code)
        self.assertFalse(
            ChatTaskSource.objects.filter(id=self.source.id, user=self.user).exists()
        )

    def test_source_create_updates_existing_signal_mapping_instead_of_duplicating(self):
        signal_project = TaskProject.objects.create(user=self.user, name="Signal A")
        signal_source = ChatTaskSource.objects.create(
            user=self.user,
            service="signal",
            channel_identifier="group.c0VHQTlGMEhRL2V5TGdtdkt4MjNoaGE5VnA3bURSaHBxMjMvcm9WU1piST0=",
            project=signal_project,
            settings={"match_mode": "strict"},
            enabled=True,
        )
        target_project = TaskProject.objects.create(user=self.user, name="Signal B")
        response = self.client.post(
            reverse("tasks_settings"),
            {
                "action": "source_create",
                "service": "signal",
                "channel_identifier": "group.c0VHQTlGMEhRL2V5TGdtdkt4MjNoaGE5VnA3bURSaHBxMjMvcm9WU1piST0=",
                "project_id": str(target_project.id),
                "source_match_mode": "strict",
                "source_require_prefix": "1",
                "source_derive_enabled": "1",
                "source_completion_enabled": "1",
                "source_ai_title_enabled": "1",
            },
            follow=True,
        )
        self.assertEqual(200, response.status_code)
        self.assertEqual(
            1,
            ChatTaskSource.objects.filter(
                user=self.user,
                service="signal",
                channel_identifier="group.c0VHQTlGMEhRL2V5TGdtdkt4MjNoaGE5VnA3bURSaHBxMjMvcm9WU1piST0=",
            ).count(),
        )
        signal_source.refresh_from_db()
        self.assertEqual(target_project.id, signal_source.project_id)


@override_settings(TASK_DERIVATION_USE_AI=False)
class TaskAutoBootstrapTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "task-auto-user", "task-auto@example.com", "x"
        )
        self.person = Person.objects.create(user=self.user, name="Bootstrap Chat")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="whatsapp",
            identifier="120363402761690215@g.us",
        )
        self.session = ChatSession.objects.create(
            user=self.user, identifier=self.identifier
        )

    def test_task_message_auto_creates_project_and_source(self):
        msg = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="task: ship alpha",
            ts=1000,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        with patch("core.tasks.engine.send_message_raw", new=AsyncMock()):
            async_to_sync(process_inbound_task_intelligence)(msg)
        source = ChatTaskSource.objects.filter(
            user=self.user,
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
            enabled=True,
        ).first()
        self.assertIsNotNone(source)
        self.assertTrue(
            TaskProject.objects.filter(user=self.user, id=source.project_id).exists()
        )
        self.assertEqual(1, DerivedTask.objects.filter(user=self.user).count())


class TaskProjectDeleteGuardTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "task-delete-user", "task-delete@example.com", "x"
        )
        self.client.force_login(self.user)
        self.project = TaskProject.objects.create(user=self.user, name="Delete Me")
        self.source = ChatTaskSource.objects.create(
            user=self.user,
            service="signal",
            channel_identifier="+15550000001",
            project=self.project,
            enabled=True,
        )

    def test_project_delete_requires_exact_confirmation(self):
        response = self.client.post(
            reverse("tasks_hub"),
            {
                "action": "project_delete",
                "project_id": str(self.project.id),
                "confirm_name": "wrong",
            },
            follow=True,
        )
        self.assertEqual(200, response.status_code)
        self.assertTrue(
            TaskProject.objects.filter(id=self.project.id, user=self.user).exists()
        )

    def test_project_delete_reseeds_default_mapping(self):
        response = self.client.post(
            reverse("tasks_hub"),
            {
                "action": "project_delete",
                "project_id": str(self.project.id),
                "confirm_name": "Delete Me",
            },
            follow=True,
        )
        self.assertEqual(200, response.status_code)
        self.assertFalse(
            TaskProject.objects.filter(id=self.project.id, user=self.user).exists()
        )
        self.assertTrue(
            ChatTaskSource.objects.filter(
                user=self.user,
                service="signal",
                channel_identifier="+15550000001",
                enabled=True,
            ).exists()
        )


class TaskHubEmptyProjectVisibilityTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "task-hub-user", "task-hub@example.com", "x"
        )
        self.client.force_login(self.user)
        self.empty = TaskProject.objects.create(user=self.user, name="Empty")
        self.used = TaskProject.objects.create(user=self.user, name="Used")
        DerivedTask.objects.create(
            user=self.user,
            project=self.used,
            title="Visible Task",
            source_service="web",
            source_channel="web-1",
            reference_code="1",
            status_snapshot="open",
        )

    def test_tasks_hub_hides_empty_projects_by_default(self):
        response = self.client.get(reverse("tasks_hub"))
        self.assertEqual(200, response.status_code)
        projects = list(response.context["projects"])
        self.assertEqual(["Used"], [str(row.name) for row in projects])

    def test_tasks_hub_can_show_empty_projects(self):
        response = self.client.get(reverse("tasks_hub"), {"show_empty": "1"})
        self.assertEqual(200, response.status_code)
        names = sorted(str(row.name) for row in response.context["projects"])
        self.assertEqual(["Empty", "Used"], names)


class MockProviderSettingsTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "task-provider-user", "task-provider@example.com", "x"
        )
        self.client.force_login(self.user)
    
    def test_provider_update_persists_mock_enabled_state(self):
        response = self.client.post(
            reverse("tasks_settings"),
            {
                "action": "provider_update",
                "provider": "mock",
                "enabled": "1",
            },
            follow=True,
        )
        self.assertEqual(200, response.status_code)
        cfg = TaskProviderConfig.objects.get(user=self.user, provider="mock")
        self.assertTrue(cfg.enabled)
        self.assertEqual({}, dict(cfg.settings or {}))
