import json
import os
import uuid
from contextlib import contextmanager
from datetime import timedelta, timezone
from decimal import Decimal
from ipaddress import ip_network
from socket import AF_INET, AF_INET6, IPPROTO_TCP, SOCK_STREAM
from typing import Any, Dict, List, Optional, Type, Union
from unittest.mock import patch

from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser
from django.db import connection
from django.utils.dateparse import parse_date, parse_datetime

import psycopg2
from freezegun import freeze_time
from pytest_unordered import unordered

from baserow.contrib.database.fields.field_helpers import (
    construct_all_possible_field_kwargs,
)
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.models import SelectOption
from baserow.contrib.database.models import Database
from baserow.contrib.database.rows.handler import RowHandler
from baserow.core.action.models import Action
from baserow.core.action.registries import ActionType
from baserow.core.models import Workspace

User = get_user_model()


def _parse_datetime(datetime):
    return parse_datetime(datetime).replace(tzinfo=timezone.utc)


def _parse_date(date):
    return parse_date(date)


def is_dict_subset(subset: dict, superset: dict) -> bool:
    if isinstance(subset, dict):
        return all(
            key in superset and is_dict_subset(val, superset[key])
            for key, val in subset.items()
        )

    if isinstance(subset, list) or isinstance(subset, set):
        return all(
            any(is_dict_subset(subitem, superitem) for superitem in superset)
            for subitem in subset
        )

    return subset == superset


def uuid4_generator():
    """
    A generator that will yield a new uuid4 each time it is called. This is used to
    patch the uuid4 function so that we can predict the uuids that will be generated
    in tests.
    """

    base_uuid = uuid.UUID("00000000-0000-4000-8000-000000000000")
    counter = 1
    while True:
        yield uuid.UUID(int=base_uuid.int + counter)
        counter += 1


def setup_interesting_test_table(
    data_fixture,
    user: Optional[AbstractUser] = None,
    database: Optional[Database] = None,
    name: Optional[str] = None,
    file_suffix: Optional[str] = None,
    user_kwargs: Optional[Dict[str, Any]] = None,
):
    """
    Constructs a testing table with every field type, their sub types and any other
    interesting baserow edge cases worth testing when writing a comprehensive "does this
    feature work with all the baserow fields" test.

    :param data_fixture: The baserow testing data_fixture object
    :return:
    """

    if user_kwargs is None:
        user_kwargs = {}

    if user is None:
        user = data_fixture.create_user(**user_kwargs)

    if database is None:
        database = data_fixture.create_database_application(user=user)

    file_suffix = file_suffix or ""

    try:
        user2, user3 = User.objects.filter(
            email__in=["user2@example.com", "user3@example.com"]
        ).order_by("id")
    except ValueError:
        user2 = data_fixture.create_user(
            workspace=database.workspace, email="user2@example.com"
        )
        user3 = data_fixture.create_user(
            workspace=database.workspace, email="user3@example.com"
        )

    table = data_fixture.create_database_table(
        database=database, user=user, name=name or "interesting_test_table"
    )
    link_table = data_fixture.create_database_table(
        database=database, user=user, name="link_table"
    )
    other_table_primary_text_field = data_fixture.create_text_field(
        table=link_table, name="text_field", primary=True
    )
    decimal_link_table = data_fixture.create_database_table(
        database=database, user=user, name="decimal_link_table"
    )
    file_link_table = data_fixture.create_database_table(
        database=database, user=user, name="file_link_table"
    )
    all_possible_kwargs_per_type = construct_all_possible_field_kwargs(
        table, link_table, decimal_link_table, file_link_table
    )
    name_to_field_id = {}
    i = 0
    other_table_primary_decimal_field = data_fixture.create_number_field(
        table=decimal_link_table,
        name="decimal_field",
        primary=True,
        number_decimal_places=3,
        number_negative=True,
    )
    other_table_primary_file_field = data_fixture.create_file_field(
        table=file_link_table,
        name="file_field",
        primary=True,
    )
    handler = FieldHandler()
    for field_type_name, all_possible_kwargs in all_possible_kwargs_per_type.items():
        for kwargs in all_possible_kwargs:
            field = handler.create_field(
                user=user,
                table=table,
                type_name=field_type_name,
                order=i,
                **kwargs,
            )
            i += 1
            name_to_field_id[kwargs["name"]] = field.id
    row_handler = RowHandler()

    datetime = _parse_datetime("2020-02-01 01:23")
    date = _parse_date("2020-02-01")
    option_c, option_d, option_e = (
        SelectOption.objects.filter(field_id=name_to_field_id["multiple_select"])
        .order_by("id")
        .values_list("id", flat=True)
    )

    file_suffix = file_suffix or ""

    values = {
        "text": "text",
        "long_text": "long_text",
        "url": "https://www.google.com",
        "email": "test@example.com",
        "negative_int": -1,
        "positive_int": 1,
        "negative_decimal": Decimal("-1.2"),
        "positive_decimal": Decimal("1.2"),
        "rating": 3,
        "boolean": "True",
        "datetime_us": datetime,
        "date_us": date,
        "datetime_eu": datetime,
        "date_eu": date,
        "datetime_eu_tzone_visible": datetime,
        "datetime_eu_tzone_hidden": datetime,
        "last_modified_datetime_us": None,
        "last_modified_date_us": None,
        "last_modified_datetime_eu": None,
        "last_modified_date_eu": None,
        "last_modified_datetime_eu_tzone": None,
        "created_on_datetime_us": None,
        "created_on_date_us": None,
        "created_on_datetime_eu": None,
        "created_on_date_eu": None,
        "created_on_datetime_eu_tzone": None,
        "last_modified_by": None,
        "created_by": None,
        "duration_hm": timedelta(hours=1, minutes=1),
        "duration_hms": timedelta(hours=1, minutes=1, seconds=6),
        "duration_hms_s": timedelta(hours=1, minutes=1, seconds=6.6),
        "duration_hms_ss": timedelta(hours=1, minutes=1, seconds=6.66),
        "duration_hms_sss": timedelta(hours=1, minutes=1, seconds=6.666),
        "duration_dh": timedelta(days=1, hours=1),
        "duration_dhm": timedelta(days=1, hours=1, minutes=1),
        "duration_dhms": timedelta(days=1, hours=1, minutes=1, seconds=6),
        # We will setup link rows manually later
        "link_row": None,
        "self_link_row": None,
        "link_row_without_related": None,
        "decimal_link_row": None,
        "file_link_row": None,
        "file": [
            {
                "name": f"hashed{file_suffix}_name.txt",
                "visible_name": "a.txt",
                "is_image": False,
                "size": 0,
                "mime_type": "text/plain",
                "image_width": None,
                "image_height": None,
                "uploaded_at": "2020-02-01 01:23",
            },
            {
                "name": f"other{file_suffix}_name.txt",
                "visible_name": "b.txt",
                "is_image": False,
                "size": 0,
                "mime_type": "text/plain",
                "image_width": None,
                "image_height": None,
                "uploaded_at": "2020-02-01 01:23",
            },
        ],
        "single_select": SelectOption.objects.get(
            value="A", field_id=name_to_field_id["single_select"]
        ).id,
        "multiple_select": [option_d, option_c, option_e],
        "multiple_collaborators": [
            {"id": user2.id, "name": user2.first_name},
            {"id": user3.id, "name": user3.first_name},
        ],
        "phone_number": "+4412345678",
    }

    with freeze_time("2020-02-01 01:23"):
        data_fixture.create_user_file(
            original_name=f"a.txt",
            unique=f"hashed{file_suffix}",
            sha256_hash="name",
        )
        data_fixture.create_user_file(
            original_name=f"b.txt",
            unique=f"other{file_suffix}",
            sha256_hash="name",
        )

    missing_fields = (
        set(name_to_field_id.keys())
        - set(values.keys())
        - set([f["name"] for f in all_possible_kwargs_per_type["formula"]])
        - {"lookup", "count", "rollup", "uuid", "autonumber"}
    )
    assert missing_fields == set(), (
        "Please update the dictionary above with interesting test values for your new "
        f"field type. In the values dict you are missing the fields {missing_fields}."
    )
    row_values = {}
    for field_type, val in values.items():
        if val is not None:
            row_values[f"field_{name_to_field_id[field_type]}"] = val
    # Make a blank row to test empty field conversion also.

    # We freeze time here so that we know what the values of the last_modified and
    # created_on field types are going to be. Freezing the datetime will also freeze
    # the current daylight savings time information.
    with freeze_time("2021-01-02 12:00"), patch(
        "uuid.uuid4", side_effect=uuid4_generator()
    ):
        blank_row, row = row_handler.create_rows(user, table, [{}, row_values])

    # Setup the link rows
    linked_row_1 = row_handler.create_row(
        user=user,
        table=link_table,
        values={
            other_table_primary_text_field.id: "linked_row_1",
        },
    )
    linked_row_2 = row_handler.create_row(
        user=user,
        table=link_table,
        values={
            other_table_primary_text_field.id: "linked_row_2",
        },
    )
    linked_row_3 = row_handler.create_row(
        user=user,
        table=link_table,
        values={
            other_table_primary_text_field.id: None,
        },
    )
    linked_row_4 = row_handler.create_row(
        user=user,
        table=decimal_link_table,
        values={
            other_table_primary_decimal_field.id: "1.234",
        },
    )
    linked_row_5 = row_handler.create_row(
        user=user,
        table=decimal_link_table,
        values={
            other_table_primary_decimal_field.id: "-123.456",
        },
    )
    linked_row_6 = row_handler.create_row(
        user=user,
        table=decimal_link_table,
        values={
            other_table_primary_decimal_field.id: None,
        },
    )
    with freeze_time("2020-01-01 12:00"):
        user_file_1 = data_fixture.create_user_file(
            original_name=f"name{file_suffix}.txt",
            unique=f"test{file_suffix}",
            sha256_hash=f"hash{file_suffix}",
        )
    linked_row_7 = row_handler.create_row(
        user=user,
        table=file_link_table,
        values={
            other_table_primary_file_field.id: [{"name": user_file_1.name}],
        },
    )
    linked_row_8 = row_handler.create_row(
        user=user,
        table=file_link_table,
        values={
            other_table_primary_file_field.id: None,
        },
    )

    link_row_field_id = name_to_field_id["link_row"]
    link_row_field_without_related_id = name_to_field_id["link_row_without_related"]
    self_link_row_field_id = name_to_field_id["self_link_row"]
    decimal_row_field_id = name_to_field_id["decimal_link_row"]
    file_link_row_id = name_to_field_id["file_link_row"]
    with freeze_time("2021-01-02 12:00"):
        handler = RowHandler()
        row = handler.update_row_by_id(
            user,
            table,
            row.id,
            {
                f"field_{link_row_field_id}": [
                    linked_row_1.id,
                    linked_row_2.id,
                    linked_row_3.id,
                ],
                f"field_{self_link_row_field_id}": [blank_row.id],
                f"field_{link_row_field_without_related_id}": [
                    linked_row_1.id,
                    linked_row_2.id,
                ],
                f"field_{decimal_row_field_id}": [
                    linked_row_4.id,
                    linked_row_5.id,
                    linked_row_6.id,
                ],
                f"field_{file_link_row_id}": [linked_row_7.id, linked_row_8.id],
            },
        )

    context = {"user2": user2, "user3": user3}

    return table, user, row, blank_row, context


def setup_interesting_test_database(
    data_fixture,
    user: Optional[AbstractUser] = None,
    workspace: Optional[Workspace] = None,
    database: Optional[Database] = None,
    name: Optional[str] = None,
    user_kwargs=None,
):
    if user_kwargs is None:
        user_kwargs = {}

    if user is None:
        user = data_fixture.create_user(**user_kwargs)

    if database is None:
        database = data_fixture.create_database_application(
            user=user, workspace=workspace, name=name
        )

    for table_name in ["A", "B", "C"]:
        setup_interesting_test_table(
            data_fixture,
            user=user,
            database=database,
            name=table_name,
            file_suffix=table_name,
        )

    return database


@contextmanager
def register_instance_temporarily(registry, instance):
    """
    A context manager to allow tests to register a new instance into a Baserow
    registry which will then unregister the instance afterwards to ensure the global
    registries are kept clean between tests.
    """

    registry.register(instance)
    try:
        yield instance
    finally:
        registry.unregister(instance)


def assert_undo_redo_actions_are_valid(
    actions: List[Action], expected_action_types: List[Type[ActionType]]
):
    assert len(actions) == len(
        expected_action_types
    ), f"Expected {len(actions)} actions but got {len(expected_action_types)} action_types"

    for action, expected_action_type in zip(actions, expected_action_types):
        assert (
            action.type == expected_action_type.type
        ), f"Action expected of type {expected_action_type} but got {action}"
        assert (
            action is not None
        ), f"Action is None, but should be of type {expected_action_type}"
        assert action.error is None, f"Action has error: {action.error}"


def assert_undo_redo_actions_fails_with_error(
    actions: List[Action], expected_action_types: List[Type[ActionType]]
):
    assert actions, "Actions list should not be empty"

    for action, expected_action_type in zip(actions, expected_action_types):
        assert action, f"Action is None, but should be of type {expected_action_type}"
        assert action.error is not None, "Action has no error, but should have one"


@contextmanager
def independent_test_db_connection():
    d = connection.settings_dict
    conn = psycopg2.connect(
        host=d["HOST"],
        database=d["NAME"],
        user=d["USER"],
        password=d["PASSWORD"],
        port=d["PORT"],
    )
    conn.autocommit = False
    yield conn
    conn.close()


def assert_serialized_field_values_are_the_same(
    value_1, value_2, ordered=False, field_name=None
):
    if isinstance(value_1, list) and not ordered:
        assert unordered(value_1, value_2)
    else:
        assert value_1 == value_2, f"{field_name or 'error'}: {value_1} != {value_2}"


def extract_serialized_field_value(field_value):
    if not field_value:
        return field_value

    def extract_value(value):
        if isinstance(value, dict):
            if "name" in value:
                return value["name"]
            if "url" in value:
                return (value.get("label") or "") + " (" + value["url"] + ")"
            return value["value"]
        return value

    if isinstance(field_value, list):
        return [extract_value(value) for value in field_value]

    return extract_value(field_value)


def assert_serialized_rows_contain_same_values(row_1, row_2):
    for field_name, row_field_value in row_1.items():
        row_1_value = extract_serialized_field_value(row_field_value)
        row_2_value = extract_serialized_field_value(row_2[field_name])
        assert_serialized_field_values_are_the_same(
            row_1_value, row_2_value, field_name=field_name
        )


# The httpretty stub implementation of socket.getaddrinfo is incorrect and doesn't
# return an IP causing advocate to fail, instead we patch to fix this.
def stub_getaddrinfo(host, port, family=None, socktype=None, proto=None, flags=None):
    try:
        ip_network(host)
        ip = host
    except ValueError:
        ip = "1.1.1.1"
    return [
        (AF_INET, SOCK_STREAM, IPPROTO_TCP, host, (ip, port)),
        (AF_INET6, SOCK_STREAM, IPPROTO_TCP, "", (ip, port)),
    ]


class AnyInt(int):
    """
    A class that can be used to check if a value is an int. Useful in tests when
    you don't care about ID, but you want to check all other values.
    """

    def __eq__(self, other):
        return isinstance(other, int)


class AnyStr(str):
    """
    A class that can be used to check if a value is an str. Useful in tests when
    you don't care about a string, but you want to check all other values.
    """

    def __eq__(self, other):
        return isinstance(other, str)


def load_test_cases(name: str) -> Union[List, Dict]:
    """
    Load test data from the global cases directory. These cases are used to run the
    same tests with the same data in both frontend and backend.
    :param name:
    :return:
    """

    current_file_path = os.path.abspath(__file__)
    current_directory = os.path.dirname(current_file_path)
    cases_path = os.path.join(current_directory, "../../../../tests/cases/")
    file_path = os.path.join(cases_path, f"{name}.json")

    with open(file_path, "r") as file:
        return json.load(file)