"""Contacts backend tests."""

import os

import httmock
from rq import SimpleWorker

from django.core import management
from django.urls import reverse
from django.utils import timezone

import django_rq

from modoboa.admin import factories as admin_factories
from modoboa.core import models as core_models
from modoboa.lib.tests import ModoAPITestCase

from . import factories
from . import mocks
from . import models


class TestDataMixin:
    """Create some data."""

    @classmethod
    def setUpTestData(cls):
        """Create test data."""
        super().setUpTestData()
        admin_factories.populate_database()
        cls.user = core_models.User.objects.get(username="user@test.com")
        cls.addressbook = cls.user.addressbook_set.first()
        cls.category = factories.CategoryFactory(user=cls.user, name="Family")
        cls.contact = factories.ContactFactory(
            addressbook=cls.addressbook,
            emails=["homer@simpson.com"],
            phone_numbers=["01234567889"],
        )
        factories.ContactFactory(
            addressbook=cls.addressbook,
            first_name="Marge",
            emails=["marge@simpson.com"],
            categories=[cls.category],
        )
        factories.ContactFactory(
            addressbook=cls.addressbook, first_name="Bart", emails=["bart@simpson.com"]
        )

    def setUp(self, *args, **kwargs):
        """Initiate test context."""
        self.client.force_authenticate(self.user)
        self.set_global_parameter(
            "server_location", "http://example.test/radicale/", app="calendars"
        )

    def enable_cdav_sync(self):
        """Enable sync. for user."""
        url = reverse("v2:parameter-user-detail", args=["contacts"])
        with httmock.HTTMock(mocks.options_mock, mocks.mkcol_mock):
            response = self.client.put(
                url,
                {
                    "enable_carddav_sync": True,
                    "sync_frequency": 300,
                },
                format="json",
            )
        self.assertEqual(response.status_code, 200)

    def disable_cdav_sync(self):
        url = reverse("v2:parameter-user-detail", args=["contacts"])
        response = self.client.put(
            url,
            {
                "enable_carddav_sync": False,
                "sync_frequency": 300,
            },
            format="json",
        )
        self.assertEqual(response.status_code, 200)


class ViewsTestCase(TestDataMixin, ModoAPITestCase):
    """Check views."""

    def test_user_settings(self):
        """Check that remote collection creation request is sent."""
        # 1. Addressbook with contacts must be synced manuelle
        self.client.force_authenticate(self.user)
        self.enable_cdav_sync()
        self.addressbook.refresh_from_db()
        self.assertIs(self.addressbook.last_sync, None)

        # 2. Addressbook with no contacts can be considered synced
        user = core_models.User.objects.get(username="user@test2.com")
        abook = user.addressbook_set.first()
        self.client.force_authenticate(user)
        abook.refresh_from_db()
        self.assertIs(abook.last_sync, None)
        # Now enable sync.
        self.enable_cdav_sync()
        abook.refresh_from_db()
        self.assertIsNot(abook.last_sync, None)
        # And disable sync.
        self.disable_cdav_sync()
        abook.refresh_from_db()
        self.assertIs(abook.last_sync, None)


class AddressBookViewSetTestCase(TestDataMixin, ModoAPITestCase):
    """Address book ViewSet tests."""

    def test_default(self):
        """Test default endpoint."""
        response = self.client.get(reverse("api:addressbook-default"))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data["name"], "Contacts")

    def test_sync_to_cdav(self):
        """Test sync to CardDAV endpoint."""
        data = {"username": self.user.username, "password": "toto"}
        response = self.client.post(reverse("core:login"), data)
        self.enable_cdav_sync()
        with httmock.HTTMock(
            mocks.options_mock, mocks.mkcol_mock, mocks.put_mock, mocks.propfind_mock
        ):
            response = self.client.get(reverse("api:addressbook-sync-to-cdav"))
        self.assertEqual(response.status_code, 200)
        abook = self.user.addressbook_set.first()
        self.assertIsNot(abook.sync_token, None)
        contact = abook.contact_set.first()
        self.assertIsNot(contact.etag, None)

    def test_sync_from_cdav(self):
        """Test sync from CardDAV endpoint."""
        data = {"username": self.user.username, "password": "toto"}
        response = self.client.post(reverse("core:login"), data)
        self.enable_cdav_sync()
        last_sync = timezone.now()
        self.user.addressbook_set.update(last_sync=last_sync)
        addressbook = self.user.addressbook_set.first()
        self.assertEqual(addressbook.sync_token, "")
        with httmock.HTTMock(mocks.options_mock, mocks.report_mock, mocks.get_mock):
            response = self.client.get(reverse("api:addressbook-sync-from-cdav"))
            queue = django_rq.get_queue("modoboa")
            worker = SimpleWorker([queue], connection=queue.connection)
            worker.work(burst=True)
        self.assertEqual(response.status_code, 200)
        addressbook.refresh_from_db()
        self.assertNotEqual(addressbook.sync_token, "")


class CategoryViewSetTestCase(TestDataMixin, ModoAPITestCase):
    """Category ViewSet tests."""

    def test_get_categories(self):
        """Check category list endpoint."""
        url = reverse("api:category-list")
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 1)

    def test_create_category(self):
        """Create a new category."""
        url = reverse("api:category-list")
        data = {"name": "Amitiés"}
        response = self.client.post(url, data, format="json")
        self.assertEqual(response.status_code, 201)

    def test_update_category(self):
        """Update a category."""
        url = reverse("api:category-detail", args=[self.category.pk])
        data = {"name": "Test"}
        response = self.client.put(url, data, format="json")
        self.assertEqual(response.status_code, 200)
        self.category.refresh_from_db()
        self.assertEqual(self.category.name, "Test")

    def test_delete_category(self):
        """Try to delete a category."""
        url = reverse("api:category-detail", args=[self.category.pk])
        response = self.client.delete(url)
        self.assertEqual(response.status_code, 204)
        with self.assertRaises(models.Category.DoesNotExist):
            self.category.refresh_from_db()


class ContactViewSetTestCase(TestDataMixin, ModoAPITestCase):
    """Contact ViewSet tests."""

    def test_contact_list(self):
        """Check contact list endpoint."""
        url = reverse("api:contact-list")
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data["results"]), 3)

        response = self.client.get(f"{url}?search=homer")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data["results"]), 1)

    def test_filter_by_category(self):
        """Check filtering."""
        url = reverse("api:contact-list")
        response = self.client.get(f"{url}?category={self.category.name}")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data["results"]), 1)

    def test_create_contact(self):
        """Create a new contact."""
        data = {"username": self.user.username, "password": "toto"}
        response = self.client.post(reverse("core:login"), data)
        self.enable_cdav_sync()
        self.user.addressbook_set.update(last_sync=timezone.now())
        data = {
            "first_name": "Magie",
            "last_name": "Simpson",
            "emails": [{"address": "magie@simpson.com", "type": "home"}],
            "phone_numbers": [{"number": "+33123456789", "type": "home"}],
        }
        url = reverse("api:contact-list")
        with httmock.HTTMock(mocks.options_mock, mocks.put_mock):
            response = self.client.post(url, data, format="json")
        self.assertEqual(response.status_code, 201)
        contact = models.Contact.objects.get(pk=response.data["pk"])
        self.assertEqual(contact.emails.first().address, "magie@simpson.com")
        self.assertEqual(
            contact.phone_numbers.first().number,
            response.data["phone_numbers"][0]["number"],
        )
        self.assertEqual(contact.display_name, "Magie Simpson")
        self.assertIsNot(contact.etag, None)

    def test_create_contact_quick(self):
        """Create a contact with minimal information."""
        data = {"emails": [{"address": "magie@simpson.com", "type": "home"}]}
        url = reverse("api:contact-list")
        response = self.client.post(url, data, format="json")
        self.assertEqual(response.status_code, 400)
        self.assertEqual(
            response.data["display_name"][0], "Name or display name required"
        )

        data["display_name"] = "Magie Simpson"
        response = self.client.post(url, data, format="json")
        self.assertEqual(response.status_code, 201)

    def test_create_contact_with_category(self):
        """Create a new contact with a category."""
        data = {
            "first_name": "Magie",
            "last_name": "Simpson",
            "emails": [{"address": "magie@simpson.com", "type": "home"}],
            "categories": [self.category.pk],
        }
        url = reverse("api:contact-list")
        response = self.client.post(url, data, format="json")
        self.assertEqual(response.status_code, 201)
        contact = models.Contact.objects.get(pk=response.data["pk"])
        self.assertEqual(contact.categories.first(), self.category)

    def test_update_contact(self):
        """Update existing contact."""
        data = {"username": self.user.username, "password": "toto"}
        self.client.post(reverse("core:login"), data)
        self.enable_cdav_sync()
        self.user.addressbook_set.update(last_sync=timezone.now())
        url = reverse("api:contact-detail", args=[self.contact.pk])
        email_pk = self.contact.emails.first().pk
        data = {
            "first_name": "Homer 1",
            "last_name": "Simpson",
            "emails": [
                {"address": "duff@simpson.com", "type": "work"},
                {"address": "homer@simpson.com", "type": "other"},
            ],
            "phone_numbers": [
                {"number": "+33123456789", "type": "home"},
                {"number": "01234567889", "type": "work"},
            ],
            "categories": [self.category.pk],
        }
        with httmock.HTTMock(mocks.options_mock, mocks.put_mock, mocks.report_mock):
            response = self.client.put(url, data, format="json")
        self.assertEqual(response.status_code, 200)

        self.contact.refresh_from_db()
        self.assertEqual(self.contact.first_name, "Homer 1")
        self.assertEqual(self.contact.emails.count(), 2)
        self.assertEqual(models.EmailAddress.objects.get(pk=email_pk).type, "other")
        self.assertEqual(self.contact.phone_numbers.count(), 2)
        self.assertEqual(self.contact.categories.first(), self.category)
        self.assertIsNot(self.contact.etag, None)

        data["emails"].pop(1)
        data["phone_numbers"].pop(1)
        with httmock.HTTMock(mocks.options_mock, mocks.put_mock, mocks.report_mock):
            response = self.client.put(url, data, format="json")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(self.contact.emails.count(), 1)
        self.assertEqual(self.contact.phone_numbers.count(), 1)

    def test_delete_contact(self):
        """Try to delete a contact."""
        data = {"username": self.user.username, "password": "toto"}
        response = self.client.post(reverse("core:login"), data)
        self.enable_cdav_sync()
        url = reverse("api:contact-detail", args=[self.contact.pk])
        with httmock.HTTMock(mocks.options_mock, mocks.delete_mock):
            response = self.client.delete(url)
        self.assertEqual(response.status_code, 204)
        with self.assertRaises(models.Contact.DoesNotExist):
            self.contact.refresh_from_db()


class EmailAddressViewSetTestCase(TestDataMixin, ModoAPITestCase):
    """EmailAddressViewSet tests."""

    def test_emails_list(self):
        """Check list endpoint."""
        url = reverse("api:emailaddress-list")
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 3)

        response = self.client.get(f"{url}?search=homer")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 1)

        response = self.client.get(f"{url}?search=Marge")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 1)

        response = self.client.get(f"{url}?search=Simpson")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 3)


class ImportTestCase(TestDataMixin, ModoAPITestCase):

    def setUp(self):
        super().setUp()
        self.path = os.path.join(
            os.path.abspath(os.path.dirname(__file__)), "test_data/outlook_export.csv"
        )
        self.wrong_path = os.path.join(
            os.path.abspath(os.path.dirname(__file__)), "test_data/unknown_export.csv"
        )

    def test_import_wrong_addressbook(self):
        with self.assertRaises(management.base.CommandError) as ctx:
            management.call_command("import_contacts", "error@test.com", self.path)
        self.assertEqual(
            str(ctx.exception), "Address Book for email 'error@test.com' not found"
        )

    def test_import_unknown_backend(self):
        with self.assertRaises(management.base.CommandError) as ctx:
            management.call_command("import_contacts", "user@test.com", self.wrong_path)
        self.assertEqual(str(ctx.exception), "Failed to detect backend to use")

    def test_import_from_outlook_auto(self):
        management.call_command("import_contacts", "user@test.com", self.path)
        address = models.EmailAddress.objects.get(address="toto@titi.com")
        models.PhoneNumber.objects.get(number="12345678")
        self.assertEqual(address.contact.first_name, "Toto Tata")
        self.assertEqual(address.contact.addressbook.user.email, "user@test.com")
        self.assertEqual(address.contact.address, "Street 1 Street 2")

    def test_import_from_outlook(self):
        management.call_command(
            "import_contacts",
            "user@test.com",
            self.path,
            backend="outlook",
        )
        address = models.EmailAddress.objects.get(address="toto@titi.com")
        models.PhoneNumber.objects.get(number="12345678")
        self.assertEqual(address.contact.first_name, "Toto Tata")
        self.assertEqual(address.contact.addressbook.user.email, "user@test.com")
        self.assertEqual(address.contact.address, "Street 1 Street 2")

    def test_import_and_carddav_sync(self):
        with httmock.HTTMock(mocks.options_mock, mocks.put_mock):
            management.call_command(
                "import_contacts",
                "user@test.com",
                self.path,
                carddav_password="Toto1234",
            )
        address = models.EmailAddress.objects.get(address="toto@titi.com")
        models.PhoneNumber.objects.get(number="12345678")
        self.assertEqual(address.contact.first_name, "Toto Tata")
        self.assertEqual(address.contact.addressbook.user.email, "user@test.com")
        self.assertEqual(address.contact.address, "Street 1 Street 2")
