from datetime import date

from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase

from documents.models import CustomField
from documents.models import CustomFieldInstance
from documents.models import Document
from documents.tests.utils import DirectoriesMixin


class TestCustomField(DirectoriesMixin, APITestCase):
    ENDPOINT = "/api/custom_fields/"

    def setUp(self):
        self.user = User.objects.create_superuser(username="temp_admin")
        self.client.force_authenticate(user=self.user)
        return super().setUp()

    def test_create_custom_field(self):
        """
        GIVEN:
            - Each of the supported data types is created
        WHEN:
            - API request to create custom metadata is made
        THEN:
            - the field is created
            - the field returns the correct fields
        """
        for field_type, name in [
            ("string", "Custom Text"),
            ("url", "Wikipedia Link"),
            ("date", "Invoiced Date"),
            ("integer", "Invoice #"),
            ("boolean", "Is Active"),
            ("float", "Average Value"),
            ("monetary", "Total Paid"),
            ("documentlink", "Related Documents"),
        ]:
            resp = self.client.post(
                self.ENDPOINT,
                data={
                    "data_type": field_type,
                    "name": name,
                },
            )
            self.assertEqual(resp.status_code, status.HTTP_201_CREATED)

            data = resp.json()

            self.assertEqual(len(data), 3)
            self.assertEqual(data["name"], name)
            self.assertEqual(data["data_type"], field_type)

    def test_create_custom_field_instance(self):
        """
        GIVEN:
            - Field of each data type is created
        WHEN:
            - API request to create custom metadata instance with each data type
        THEN:
            - the field instance is created
            - the field returns the correct fields and values
            - the field is attached to the given document
        """
        doc = Document.objects.create(
            title="WOW",
            content="the content",
            checksum="123",
            mime_type="application/pdf",
        )
        doc2 = Document.objects.create(
            title="WOW2",
            content="the content2",
            checksum="1234",
            mime_type="application/pdf",
        )
        custom_field_string = CustomField.objects.create(
            name="Test Custom Field String",
            data_type=CustomField.FieldDataType.STRING,
        )
        custom_field_date = CustomField.objects.create(
            name="Test Custom Field Date",
            data_type=CustomField.FieldDataType.DATE,
        )
        custom_field_int = CustomField.objects.create(
            name="Test Custom Field Int",
            data_type=CustomField.FieldDataType.INT,
        )
        custom_field_boolean = CustomField.objects.create(
            name="Test Custom Field Boolean",
            data_type=CustomField.FieldDataType.BOOL,
        )
        custom_field_url = CustomField.objects.create(
            name="Test Custom Field Url",
            data_type=CustomField.FieldDataType.URL,
        )
        custom_field_float = CustomField.objects.create(
            name="Test Custom Field Float",
            data_type=CustomField.FieldDataType.FLOAT,
        )
        custom_field_monetary = CustomField.objects.create(
            name="Test Custom Field Monetary",
            data_type=CustomField.FieldDataType.MONETARY,
        )
        custom_field_documentlink = CustomField.objects.create(
            name="Test Custom Field Doc Link",
            data_type=CustomField.FieldDataType.DOCUMENTLINK,
        )

        date_value = date.today()

        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_string.id,
                        "value": "test value",
                    },
                    {
                        "field": custom_field_date.id,
                        "value": date_value.isoformat(),
                    },
                    {
                        "field": custom_field_int.id,
                        "value": 3,
                    },
                    {
                        "field": custom_field_boolean.id,
                        "value": True,
                    },
                    {
                        "field": custom_field_url.id,
                        "value": "https://example.com",
                    },
                    {
                        "field": custom_field_float.id,
                        "value": 12.3456,
                    },
                    {
                        "field": custom_field_monetary.id,
                        "value": 11.10,
                    },
                    {
                        "field": custom_field_documentlink.id,
                        "value": [doc2.id],
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_200_OK)

        resp_data = resp.json()["custom_fields"]

        self.assertCountEqual(
            resp_data,
            [
                {"field": custom_field_string.id, "value": "test value"},
                {"field": custom_field_date.id, "value": date_value.isoformat()},
                {"field": custom_field_int.id, "value": 3},
                {"field": custom_field_boolean.id, "value": True},
                {"field": custom_field_url.id, "value": "https://example.com"},
                {"field": custom_field_float.id, "value": 12.3456},
                {"field": custom_field_monetary.id, "value": 11.10},
                {"field": custom_field_documentlink.id, "value": [doc2.id]},
            ],
        )

        doc.refresh_from_db()
        self.assertEqual(len(doc.custom_fields.all()), 8)

    def test_change_custom_field_instance_value(self):
        """
        GIVEN:
            - Custom field instance is created and attached to document
        WHEN:
            - API request to create change the value of the custom field
        THEN:
            - the field instance is updated
            - the field returns the correct fields and values
        """
        doc = Document.objects.create(
            title="WOW",
            content="the content",
            checksum="123",
            mime_type="application/pdf",
        )
        custom_field_string = CustomField.objects.create(
            name="Test Custom Field String",
            data_type=CustomField.FieldDataType.STRING,
        )

        self.assertEqual(CustomFieldInstance.objects.count(), 0)

        # Create
        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_string.id,
                        "value": "test value",
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(CustomFieldInstance.objects.count(), 1)
        self.assertEqual(doc.custom_fields.first().value, "test value")

        # Update
        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_string.id,
                        "value": "a new test value",
                    },
                ],
            },
            format="json",
        )
        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(CustomFieldInstance.objects.count(), 1)
        self.assertEqual(doc.custom_fields.first().value, "a new test value")

    def test_delete_custom_field_instance(self):
        """
        GIVEN:
            - Multiple custom field instances are created and attached to document
        WHEN:
            - API request to remove a field
        THEN:
            - the field instance is removed
            - the other field remains unchanged
            - the field returns the correct fields and values
        """
        doc = Document.objects.create(
            title="WOW",
            content="the content",
            checksum="123",
            mime_type="application/pdf",
        )
        custom_field_string = CustomField.objects.create(
            name="Test Custom Field String",
            data_type=CustomField.FieldDataType.STRING,
        )
        custom_field_date = CustomField.objects.create(
            name="Test Custom Field Date",
            data_type=CustomField.FieldDataType.DATE,
        )

        date_value = date.today()

        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_string.id,
                        "value": "a new test value",
                    },
                    {
                        "field": custom_field_date.id,
                        "value": date_value.isoformat(),
                    },
                ],
            },
            format="json",
        )
        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(CustomFieldInstance.objects.count(), 2)
        self.assertEqual(len(doc.custom_fields.all()), 2)

        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_date.id,
                        "value": date_value.isoformat(),
                    },
                ],
            },
            format="json",
        )
        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(CustomFieldInstance.objects.count(), 1)
        self.assertEqual(Document.objects.count(), 1)
        self.assertEqual(len(doc.custom_fields.all()), 1)
        self.assertEqual(doc.custom_fields.first().value, date_value)

    def test_custom_field_validation(self):
        """
        GIVEN:
            - Document exists with no fields
        WHEN:
            - API request to remove a field
            - API request is not valid
        THEN:
            - HTTP 400 is returned
            - No field created
            - No field attached to the document
        """
        doc = Document.objects.create(
            title="WOW",
            content="the content",
            checksum="123",
            mime_type="application/pdf",
        )
        custom_field_string = CustomField.objects.create(
            name="Test Custom Field String",
            data_type=CustomField.FieldDataType.STRING,
        )

        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_string.id,
                        # Whoops, spelling
                        "valeu": "a new test value",
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertEqual(CustomFieldInstance.objects.count(), 0)
        self.assertEqual(len(doc.custom_fields.all()), 0)

    def test_custom_field_value_url_validation(self):
        """
        GIVEN:
            - Document & custom field exist
        WHEN:
            - API request to set a field value to something which is or is not a link
        THEN:
            - HTTP 400 is returned
            - No field instance is created or attached to the document
        """
        doc = Document.objects.create(
            title="WOW",
            content="the content",
            checksum="123",
            mime_type="application/pdf",
        )
        custom_field_url = CustomField.objects.create(
            name="Test Custom Field URL",
            data_type=CustomField.FieldDataType.URL,
        )

        for value in ["not a url", "file:"]:
            with self.subTest(f"Test value {value}"):
                resp = self.client.patch(
                    f"/api/documents/{doc.id}/",
                    data={
                        "custom_fields": [
                            {
                                "field": custom_field_url.id,
                                "value": value,
                            },
                        ],
                    },
                    format="json",
                )

                self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
                self.assertEqual(CustomFieldInstance.objects.count(), 0)
                self.assertEqual(len(doc.custom_fields.all()), 0)
        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_url.id,
                        "value": "tel:+1-816-555-1212",
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_200_OK)

    def test_custom_field_value_integer_validation(self):
        """
        GIVEN:
            - Document & custom field exist
        WHEN:
            - API request to set a field value to something not an integer
        THEN:
            - HTTP 400 is returned
            - No field instance is created or attached to the document
        """
        doc = Document.objects.create(
            title="WOW",
            content="the content",
            checksum="123",
            mime_type="application/pdf",
        )
        custom_field_int = CustomField.objects.create(
            name="Test Custom Field INT",
            data_type=CustomField.FieldDataType.INT,
        )

        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_int.id,
                        "value": "not an int",
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertEqual(CustomFieldInstance.objects.count(), 0)
        self.assertEqual(len(doc.custom_fields.all()), 0)

    def test_custom_field_value_monetary_validation(self):
        """
        GIVEN:
            - Document & custom field exist
        WHEN:
            - API request to set a field value to something not a valid monetary decimal
        THEN:
            - HTTP 400 is returned
            - No field instance is created or attached to the document
        """
        doc = Document.objects.create(
            title="WOW",
            content="the content",
            checksum="123",
            mime_type="application/pdf",
        )
        custom_field_money = CustomField.objects.create(
            name="Test Custom Field MONETARY",
            data_type=CustomField.FieldDataType.MONETARY,
        )

        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_money.id,
                        # Too many places past decimal
                        "value": 12.123,
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertEqual(CustomFieldInstance.objects.count(), 0)
        self.assertEqual(len(doc.custom_fields.all()), 0)

    def test_custom_field_value_short_text_validation(self):
        """
        GIVEN:
            - Document & custom field exist
        WHEN:
            - API request to set a field value to a too long string
        THEN:
            - HTTP 400 is returned
            - No field instance is created or attached to the document
        """
        doc = Document.objects.create(
            title="WOW",
            content="the content",
            checksum="123",
            mime_type="application/pdf",
        )
        custom_field_string = CustomField.objects.create(
            name="Test Custom Field STRING",
            data_type=CustomField.FieldDataType.STRING,
        )

        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": [
                    {"field": custom_field_string.id, "value": "a" * 129},
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertEqual(CustomFieldInstance.objects.count(), 0)
        self.assertEqual(len(doc.custom_fields.all()), 0)

    def test_custom_field_not_null(self):
        """
        GIVEN:
            - Existing document
        WHEN:
            - API request with custom_fields set to null
        THEN:
            - HTTP 400 is returned
        """
        doc = Document.objects.create(
            title="WOW",
            content="the content",
            checksum="123",
            mime_type="application/pdf",
        )

        resp = self.client.patch(
            f"/api/documents/{doc.id}/",
            data={
                "custom_fields": None,
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)

    def test_symmetric_doclink_fields(self):
        """
        GIVEN:
            - Existing document
        WHEN:
            - Doc links are added or removed
        THEN:
            - Symmetrical link is created or removed as expected
        """
        doc1 = Document.objects.create(
            title="WOW1",
            content="1",
            checksum="1",
            mime_type="application/pdf",
        )
        doc2 = Document.objects.create(
            title="WOW2",
            content="the content2",
            checksum="2",
            mime_type="application/pdf",
        )
        doc3 = Document.objects.create(
            title="WOW3",
            content="the content3",
            checksum="3",
            mime_type="application/pdf",
        )
        doc4 = Document.objects.create(
            title="WOW4",
            content="the content4",
            checksum="4",
            mime_type="application/pdf",
        )
        custom_field_doclink = CustomField.objects.create(
            name="Test Custom Field Doc Link",
            data_type=CustomField.FieldDataType.DOCUMENTLINK,
        )

        # Add links, creates bi-directional
        resp = self.client.patch(
            f"/api/documents/{doc1.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_doclink.id,
                        "value": [2, 3, 4],
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(CustomFieldInstance.objects.count(), 4)
        self.assertEqual(doc2.custom_fields.first().value, [1])
        self.assertEqual(doc3.custom_fields.first().value, [1])
        self.assertEqual(doc4.custom_fields.first().value, [1])

        # Add links appends if necessary
        resp = self.client.patch(
            f"/api/documents/{doc3.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_doclink.id,
                        "value": [1, 4],
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(doc4.custom_fields.first().value, [1, 3])

        # Remove one of the links, removed on other doc
        resp = self.client.patch(
            f"/api/documents/{doc1.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_doclink.id,
                        "value": [2, 3],
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(doc2.custom_fields.first().value, [1])
        self.assertEqual(doc3.custom_fields.first().value, [1, 4])
        self.assertEqual(doc4.custom_fields.first().value, [3])

        # Removes the field entirely
        resp = self.client.patch(
            f"/api/documents/{doc1.id}/",
            data={
                "custom_fields": [],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(doc2.custom_fields.first().value, [])
        self.assertEqual(doc3.custom_fields.first().value, [4])
        self.assertEqual(doc4.custom_fields.first().value, [3])

        # If field exists on target doc but value is None
        doc5 = Document.objects.create(
            title="WOW5",
            content="the content4",
            checksum="5",
            mime_type="application/pdf",
        )
        CustomFieldInstance.objects.create(document=doc5, field=custom_field_doclink)

        resp = self.client.patch(
            f"/api/documents/{doc1.id}/",
            data={
                "custom_fields": [
                    {
                        "field": custom_field_doclink.id,
                        "value": [doc5.id],
                    },
                ],
            },
            format="json",
        )

        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(doc5.custom_fields.first().value, [1])

    def test_custom_field_filters(self):
        custom_field_string = CustomField.objects.create(
            name="Test Custom Field String",
            data_type=CustomField.FieldDataType.STRING,
        )
        custom_field_date = CustomField.objects.create(
            name="Test Custom Field Date",
            data_type=CustomField.FieldDataType.DATE,
        )
        custom_field_int = CustomField.objects.create(
            name="Test Custom Field Int",
            data_type=CustomField.FieldDataType.INT,
        )

        response = self.client.get(
            f"{self.ENDPOINT}?id={custom_field_string.id}",
        )
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        results = response.data["results"]
        self.assertEqual(len(results), 1)

        response = self.client.get(
            f"{self.ENDPOINT}?id__in={custom_field_string.id},{custom_field_date.id}",
        )
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        results = response.data["results"]
        self.assertEqual(len(results), 2)

        response = self.client.get(
            f"{self.ENDPOINT}?name__icontains=Int",
        )
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        results = response.data["results"]
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0]["name"], custom_field_int.name)