import datetime
import pytest
import sys

from django.test import (
    TestCase
)

from access.models.tenant import Tenant



def pytest_configure(config):

    print("\n--- Pytest Launch Arguments ---")
    print(f"Command-line arguments: {config.invocation_params.args}")
    print(f"Config file options: {config.getini('addopts')}")
    print("\n-------------------------------")


def pytest_pycollect_makeitem(collector, name, obj):
    """PyTest Test Creation

    Create PyTest Test Classes if the classname ends in `Test`
    and is not inheriting from django,test.TestCase.
    """

    if (
        isinstance(obj, type)
        and name.endswith("PyTest")
        and not issubclass(obj, TestCase)    # Don't pickup any django unittest.TestCase
    ):
        return pytest.Class.from_parent(parent=collector, name=name)



@pytest.fixture(autouse=True)
def enable_db_access_for_all_tests(db):    # pylint: disable=W0613:unused-argument
    pass



def pytest_generate_tests(metafunc):

    arg_values:list = None

    fixture_parameters: list = []

    parameterized_test = False

    parameterized_key: str = None

    if {'parameterized'} <= set(metafunc.fixturenames):

        for mark in metafunc.definition.own_markers:    # Skip tests markd to skip

            if mark.name == 'skip':
                return None

        for mark in getattr(metafunc.cls, 'pytestmark', []):    # Skip test suite markd to skip

            if mark.name == 'skip':
                return None

        all_fixture_parameters = metafunc.fixturenames

        fixture_parameters += ['parameterized']

        for i in range(0, len(metafunc.fixturenames)):

            if (
                str(all_fixture_parameters[i]).startswith('param_')
                and not str(all_fixture_parameters[i]).startswith('param_key_')
            ):

                fixture_parameters += [ all_fixture_parameters[i] ]


            elif str(all_fixture_parameters[i]).startswith('param_key_'):

                parameterized_key = str( all_fixture_parameters[i] ).replace('param_key_', '')

                if len(fixture_parameters) == 1:

                    fixture_parameters += [ all_fixture_parameters[i] ]

                else:

                    fixture_parameters[1] = all_fixture_parameters[i]


        parameterized_test = len(fixture_parameters) > 0


    if parameterized_test:

        values = {}


        cls = getattr(metafunc, "cls", None)

        if cls:

            for base in reversed(cls.__mro__):

                base_values = getattr(base, 'parameterized_' + parameterized_key, None)

                if isinstance(base_values, property):

                    base_values = getattr(base(), 'parameterized_' + parameterized_key, None)

                if not isinstance(base_values, dict):

                    continue

                if len(values) == 0 and len(base_values) > 0:

                    values.update(base_values)

                    continue

                for key, value in values.items():

                    if(
                        type(value) is not dict
                        or key not in base_values
                    ):

                        continue

                    if key not in values:

                        values.update({
                            key: base_values[key]
                        })

                    else:

                        values[key].update( base_values[key] )


                for key, value in base_values.items():

                    if key not in values:

                        values.update({
                            key: base_values[key]
                        })


        if values:

            ids = []

            arg_values:list = []

            for item in values.items():

                ids_name = item[0]

                item_values:tuple = ()

                length = len(item)

                is_key_value: bool = True

                if type(item[1]) is not dict:

                    continue


                item_values += ( None, None, item[0])

                for key in fixture_parameters:

                    if key in [ fixture_parameters[0], fixture_parameters[1], fixture_parameters[2], ]:
                        # these values are already defined in `item_values`
                        # fixture_parameters[0] = parameterized.
                        # fixture_parameters[1] = param_key
                        # fixture_parameters[2] = the dict name

                        continue

                    if(
                        str(key).startswith('param_')
                        and not str(key).startswith('param_key_')
                    ):

                        key = str(key).replace('param_', '')

                        if (
                            type(item[1]) is not dict
                            or item[1].get(key, 'key-does_not-exist') == 'key-does_not-exist'
                        ):

                            item_values = ()

                            continue


                        if key in item[1]:

                            item_values += ( item[1][key], )

                            if type(item[1][key]) is type:

                                ids_name += '_' + getattr(item[1][key], '__name__', 'err_generate_tests').lower()

                            else:

                                ids_name += '_' + str(item[1][key]).lower()


                if(
                    len(item_values) > 0
                    and len(fixture_parameters) == len(item_values)
                ):

                    arg_values += [ item_values ]

                    ids += [ ids_name, ]


            if len(arg_values) > 0:

                metafunc.parametrize(
                    argnames = [
                        *fixture_parameters
                    ],
                    argvalues = arg_values,
                    ids = ids,
                )



@pytest.fixture( scope = 'class')
def create_model(request, django_db_blocker):

    item = None

    with django_db_blocker.unblock():

        item = request.cls.model.objects.create(
            **request.cls.kwargs_create_item
        )

        request.cls.item = item

    yield item

    with django_db_blocker.unblock():

        item.delete()



@pytest.fixture( scope = 'class')
def create_model_random_data_five(request, django_db_blocker):
    """Create the specified model with random data

    **Note:** Dont use this fixture yet. More testing is required

    Yields:
        models.Model: Created Model
    """


    from django.db import models
    from django.core.files.base import ContentFile
    import random
    import os
    from faker import Faker

    from django.db.models.fields.reverse_related import ManyToOneRel, ManyToManyRel, OneToOneRel

    fake = Faker()

    _unique_counter = {}

    created_instances = {}

    def generate_unique_string(length=8):

        base = fake.word()

        counter = _unique_counter.get(base, 0) + 1

        _unique_counter[base] = counter

        return f"{base}_{counter}"[:length]


    def generate_random_file():

        return ContentFile(b"dummydata", name=f"{generate_unique_string(5)}.txt")

    def get_random_choice(choices):

        valid_choices = [choice[0] for choice in choices if not isinstance(choice[1], (list, tuple))]

        return random.choice(valid_choices or [None])


    def create_sample_instance(model_class, seen=None, except_fields = [], recurse_depth = 0):

        if seen is None:

            seen = set()


        recurse_depth += 1

        if recurse_depth == 3:

            return None


        if model_class in created_instances:

            return created_instances[model_class]


        if model_class in seen:

            return None


        seen.add(model_class)

        field_data = {}

        m2m_data = {}

        # Only iterate over concrete, editable fields
        for field in model_class._meta.get_fields():

            # if field.auto_created and not field.concrete:
            #     continue  # Skip reverse relations like ManyToOneRel

            if isinstance(field, (ManyToOneRel, ManyToManyRel, OneToOneRel)):

                continue  # Skip reverse relationships only


            if getattr(field, 'primary_key', False) or field.name in ['id', 'pk']:

                continue

            if field.name in except_fields:

                continue

            if isinstance(field, models.ManyToManyField):

                related_model = field.related_model

                related_instance = create_sample_instance(related_model, seen)

                if related_instance:

                    m2m_data[field.name] = [related_instance]


            elif hasattr(field, 'choices') and field.choices:

                field_data[field.name] = get_random_choice(field.choices)

            elif isinstance(field, models.CharField):

                field_data[field.name] = generate_unique_string(length=field.max_length or 12)

            elif isinstance(field, models.TextField):

                field_data[field.name] = generate_unique_string(length=200)

            elif isinstance(field, models.EmailField):

                field_data[field.name] = f"{generate_unique_string(8)}@example.com"

            elif isinstance(field, models.IntegerField):

                field_data[field.name] = random.randint(1, 99999)

            elif isinstance(field, models.FloatField):

                field_data[field.name] = random.uniform(1.0, 1000.0)

            elif isinstance(field, models.DecimalField):

                field_data[field.name] = round(random.uniform(1.0, 1000.0), field.decimal_places)

            elif isinstance(field, models.BooleanField):

                field_data[field.name] = random.choice([True, False])

            elif isinstance(field, models.DateField):

                field_data[field.name] = fake.date()

            elif isinstance(field, models.DateTimeField):

                field_data[field.name] = fake.date_time()

            elif isinstance(field, models.TimeField):

                field_data[field.name] = fake.time()

            elif isinstance(field, models.UUIDField):

                field_data[field.name] = fake.uuid4()

            elif isinstance(field, models.IPAddressField):

                field_data[field.name] = fake.ipv4()

            elif isinstance(field, models.GenericIPAddressField):

                field_data[field.name] = fake.ipv6()

            elif isinstance(field, models.SlugField):

                field_data[field.name] = fake.slug()

            elif isinstance(field, models.BinaryField):

                field_data[field.name] = os.urandom(10)

            elif isinstance(field, models.JSONField):

                field_data[field.name] = {generate_unique_string(4): random.randint(1, 10)}

            elif isinstance(field, (models.FileField, models.ImageField)):

                field_data[field.name] = generate_random_file()

            elif isinstance(field, (models.ForeignKey, models.OneToOneField)):

                related_model = field.related_model

                if model_class == related_model:

                    related_instance = create_sample_instance(related_model, seen=None, except_fields=except_fields, recurse_depth = recurse_depth)

                else:

                    related_instance = create_sample_instance(related_model, seen)


                if related_instance:

                    field_data[field.name] = related_instance


        with django_db_blocker.unblock():

            instance = model_class.objects.create(**field_data)

            created_instances[model_class] = instance

            for field_name, related_instances in m2m_data.items():

                getattr(instance, field_name).set(related_instances)


            seen.remove(model_class)


        return instance


    yield create_sample_instance


    with django_db_blocker.unblock():

        for model_class, model in created_instances.items():

            model.delete()



@pytest.fixture( scope = 'class')
def organization_one(django_db_blocker):

    item = None

    with django_db_blocker.unblock():

        random_str = datetime.datetime.now(tz=datetime.timezone.utc)

        item = Tenant.objects.create(
            name = 'org one from global' + str(random_str)
        )

    yield item

    with django_db_blocker.unblock():

        item.delete()



@pytest.fixture( scope = 'class')
def organization_two(django_db_blocker):

    item = None

    with django_db_blocker.unblock():

        random_str = datetime.datetime.now(tz=datetime.timezone.utc)

        item = Tenant.objects.create(
            name = 'org two from global' + str(random_str)
        )

    yield item

    with django_db_blocker.unblock():

        item.delete()



@pytest.fixture
def recursearray() -> dict[dict, str, any]:
    """Recursive dict lookup

    Search through a dot notation of dict paths and return the data assosiated
    with that dict.

    Args:
        obj (dict): dict to use for the recusive lookup
        key (str): the dict keys in dot notation. i.e. dict_1.dict_2
        rtn_dict (bool, optional): return the dictionary and not the value.
            Defaults to False.

    Returns:
        dict[dict, str, any]: lowest dict found. dict values are obj, key and value.

    """

    def _recursearray(obj: dict, key:str) -> dict[dict, str, any]:

        item = None

        if key in obj:
            
            return {
                'obj': obj,
                'key': key,
                'value': obj[key]
            }

        keys = []

        if '.' in key:

            keys = key.split('.')


            for k, v in obj.items():

                if k == keys[0] and isinstance(v,dict):

                    if keys[1] in v:

                        item = _recursearray(v, key.replace(keys[0]+'.', ''))

                        return {
                            'obj': item['obj'],
                            'key': item['key'],
                            'value': item['value']
                        }

                elif k == keys[0] and isinstance(v,list):

                    try:

                        list_key = int(keys[1])

                        try:

                            item = v[list_key]

                            v = item


                            if keys[2] in v:

                                item = _recursearray(v, key.replace(keys[0]+'.'+keys[1]+'.', ''))

                                return {
                                    'obj': item['obj'],
                                    'key': item['key'],
                                    'value': item['value']
                                }

                        except IndexError:

                            print( f'Index {keys[1]} does not exist. List had a length of {len(v)}', file = sys.stderr )

                            return None

                    except ValueError:

                        print( f'{keys[1]} does not appear to be a number.', file = sys.stderr )

                        return None

        return {
            'obj': obj,
            'key': key,
            'value': None
        }

    return _recursearray


@pytest.fixture( scope = 'function')
def fake_view():


    class MockView:

        class MockRequest:

            class MockUser:

                id: int

                pk: int

            user = None

            def __init__(cls, user = 0):

                if user != 0:

                    cls.user = user

                # cls.user.id = user_id
                # cls.user.pk = user_id


        _has_import: bool
        """User Permission

        get_permission_required() sets this to `True` when user has import permission.
        """

        _has_purge: bool
        """User Permission

        get_permission_required() sets this to `True` when user has purge permission.
        """

        _has_triage: bool
        """User Permission

        get_permission_required() sets this to `True` when user has triage permission.
        """

        action:str
        """ The View Action

        This must be set to whatever is occuring
        """

        request: MockRequest

        def __init__(cls,
            _has_import: bool = False,
            _has_purge: bool = False,
            _has_triage: bool = False,
            action: str = 'create',
            user = 0,
        ):

            cls.request = cls.MockRequest(
                user = user
            )

            cls._has_import = _has_import
            cls._has_purge = _has_purge
            cls._has_triage = _has_triage
            cls.action = action


    fake_view = MockView

    yield fake_view

    del fake_view
