"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 26.10.21

"""
import datetime
import json

from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID
from ema.models import Ema
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User, Team
from django.contrib.auth.models import Group
from django.contrib.gis.geos import MultiPolygon, Polygon
from django.core.exceptions import ObjectDoesNotExist
from django.test import TestCase, Client
from django.urls import reverse

from codelist.models import KonovaCode, KonovaCodeList
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction
from intervention.models import Legal, Responsibility, Intervention, Handler
from konova.management.commands.setup_data import GROUPS_DATA
from konova.models import Geometry, Deadline, DeadlineType
from konova.settings import DEFAULT_GROUP
from konova.utils.generators import generate_random_string
from user.models import UserActionLogEntry


class BaseTestCase(TestCase):
    """ Provides reusable functionality for specialized test cases

    """
    users = None
    groups = None
    superuser = None
    user = None
    intervention = None
    compensation = None
    eco_account = None
    comp_state = None
    comp_action = None
    finished_deadline = None
    codes = None

    superuser_pw = "root"
    user_pw = "root"

    class Meta:
        abstract = True

    def setUp(self) -> None:
        """ Setup data before each test run

        Returns:

        """
        super().setUp()

        self.create_users()
        self.create_groups()
        self.handler = self.create_dummy_handler()
        self.intervention = self.create_dummy_intervention()
        self.compensation = self.create_dummy_compensation()
        self.eco_account = self.create_dummy_eco_account()
        self.ema = self.create_dummy_ema()
        self.deduction = self.create_dummy_deduction()
        self.create_dummy_states()
        self.create_dummy_action()
        self.codes = self.create_dummy_codes()
        self.team = self.create_dummy_team()
        self.finished_deadline = self.create_dummy_deadline()

        # Set the default group as only group for the user
        default_group = self.groups.get(name=DEFAULT_GROUP)
        self.superuser.groups.set([default_group])

        # Create fresh logged in client and a non-logged in client (anon) for each test
        self.client_user = Client()
        self.client_user.login(username=self.superuser.username, password=self.superuser_pw)
        self.client_anon = Client()

    
    def create_users(self):
        # Create superuser and regular user
        self.superuser = User.objects.create_superuser(
            username="root",
            email="root@root.com",
            password=self.superuser_pw,
        )
        self.user = User.objects.create_user(
            username="user1",
            email="user@root.com",
            password=self.user_pw
        )
        self.users = User.objects.all()

    
    def create_groups(self):
        # Create groups
        for group_data in GROUPS_DATA:
            name = group_data.get("name")
            Group.objects.get_or_create(
                name=name,
            )
        self.groups = Group.objects.all()

    @staticmethod
    def create_dummy_string(prefix: str = ""):
        """ Create

        Returns:

        """
        return f"{prefix}{generate_random_string(3, True)}"

    def create_dummy_document(self, DocumentModel, instance):
        """ Creates a document db entry which can be used for tests

        """
        doc = DocumentModel.objects.create(
            title="TEST_doc",
            comment="",
            file=None,
            date_of_creation="1970-01-01",
            instance=instance,
        )
        return doc

    def create_dummy_intervention(self):
        """ Creates an intervention which can be used for tests

        Returns:

        """
        # Create dummy data
        # Create log entry
        action = UserActionLogEntry.get_created_action(self.superuser)
        # Create legal data object (without M2M laws first)
        legal_data = Legal.objects.create()
        # Create responsible data object
        responsibility_data = Responsibility.objects.create(
            handler=self.handler
        )
        geometry = Geometry.objects.create()
        # Finally create main object, holding the other objects
        intervention = Intervention.objects.create(
            title="Test_title",
            responsible=responsibility_data,
            legal=legal_data,
            created=action,
            geometry=geometry,
            comment="Test",
        )
        intervention.generate_access_token(make_unique=True)
        return intervention

    def create_dummy_compensation(self, interv: Intervention=None):
        """ Creates a compensation which can be used for tests

        Returns:

        """
        if not interv:
            if self.intervention is None:
                interv = self.create_dummy_intervention()
            else:
                interv = self.intervention
        # Create dummy data
        # Create log entry
        action = UserActionLogEntry.get_created_action(self.superuser)
        geometry = Geometry.objects.create()
        # Finally create main object, holding the other objects
        compensation = Compensation.objects.create(
            title="Test_title",
            intervention=interv,
            created=action,
            geometry=geometry,
            comment="Test",
        )
        return compensation

    def create_dummy_eco_account(self):
        """ Creates an eco account which can be used for tests

        Returns:

        """
        # Create dummy data
        # Create log entry
        action = UserActionLogEntry.get_created_action(self.superuser)
        geometry = Geometry.objects.create()
        # Create responsible data object
        lega_data = Legal.objects.create()
        responsible_data = Responsibility.objects.create()
        handler = self.handler
        responsible_data.handler = handler
        responsible_data.save()

        # Finally create main object, holding the other objects
        eco_account = EcoAccount.objects.create(
            title="Test_title",
            deductable_surface=500,
            legal=lega_data,
            responsible=responsible_data,
            created=action,
            geometry=geometry,
            comment="Test",
        )
        return eco_account

    def create_dummy_ema(self):
        """ Creates an ema which can be used for tests

        Returns:

        """
        # Create dummy data
        # Create log entry
        action = UserActionLogEntry.get_created_action(self.superuser)
        geometry = Geometry.objects.create()
        # Create responsible data object
        responsible_data = Responsibility.objects.create()
        responsible_data.handler = self.handler
        responsible_data.save()
        # Finally create main object, holding the other objects
        ema = Ema.objects.create(
            title="Test_title",
            responsible=responsible_data,
            created=action,
            geometry=geometry,
            comment="Test",
        )
        return ema

    def create_dummy_deduction(self, acc: EcoAccount = None, interv: Intervention = None):
        if not acc:
            acc = self.create_dummy_eco_account()
        if not interv:
            interv = self.create_dummy_intervention()

        return EcoAccountDeduction.objects.create(
            account=acc,
            intervention=interv,
            surface=100,
        )

    def create_dummy_states(self):
        """ Creates an intervention which can be used for tests

        Returns:

        """
        self.comp_state = CompensationState.objects.create(
            surface=10.00,
            biotope_type=None,
        )
        return self.comp_state

    def create_dummy_action(self):
        """ Creates an intervention which can be used for tests

        Returns:

        """
        self.comp_action = CompensationAction.objects.create(
            amount=10
        )
        return self.comp_action

    def create_dummy_codes(self):
        """ Creates some dummy KonovaCodes which can be used for testing

        Returns:

        """
        codes = KonovaCode.objects.all()
        if codes.count() == 0:
            codes = KonovaCode.objects.bulk_create([
                KonovaCode(id=1, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test1"),
                KonovaCode(id=2, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test2"),
                KonovaCode(id=3, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test3"),
                KonovaCode(id=4, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test4"),
            ])
        return codes

    def create_dummy_team(self, name: str = None):
        """ Creates a dummy team

        Returns:

        """
        if self.superuser is None:
            self.create_users()

        if not name:
            name = "Testteam"

        team = Team.objects.get_or_create(
            name=name,
            description="Testdescription",
        )[0]
        team.users.add(self.superuser)

        return team

    def create_dummy_deadline(self, type: DeadlineType = DeadlineType.FINISHED):
        """ Creates a dummy deadline.

        If type is not specified, it defaults to DeadlineType.FINISHED

        Returns:
            deadline (Deadline): A deadline
        """
        deadline = Deadline.objects.create(
            type=type,
            date="1970-01-01"
        )
        return deadline

    @staticmethod
    def create_dummy_geometry() -> MultiPolygon:
        """ Creates some geometry

        Returns:

        """
        polygon = Polygon.from_bbox((7.592449, 50.359385, 7.593382, 50.359874))
        polygon.srid = 4326
        polygon.transform(DEFAULT_SRID_RLP)
        return MultiPolygon(polygon, srid=DEFAULT_SRID_RLP)

    def create_geojson(self, geometry):
        """ Creates a default structure including geojson from a geometry

        Args:
            geometry ():

        Returns:

        """
        geom_json = {
            "features": [
                {
                    "type": "Feature",
                    "geometry": json.loads(geometry.geojson),
                }
            ]
        }
        geom_json = json.dumps(geom_json)
        return geom_json

    def create_dummy_handler(self) -> Handler:
        """ Creates a Handler

        Returns:

        """
        handler = Handler.objects.get_or_create(
            type=KonovaCode.objects.all().first(),
            detail="Test handler"
        )[0]
        return handler

    def fill_out_intervention(self, intervention: Intervention) -> Intervention:
        """ Adds all required (dummy) data to an intervention

        Args:
            intervention (Intervention): The intervention which shall be filled out

        Returns:
            intervention (Intervention): The modified intervention
        """
        intervention.responsible.registration_office = KonovaCode.objects.get(id=1)
        intervention.responsible.conservation_office = KonovaCode.objects.get(id=2)
        intervention.responsible.registration_file_number = "test"
        intervention.responsible.conservation_file_number = "test"
        intervention.responsible.handler = self.handler
        intervention.responsible.save()
        intervention.legal.registration_date = datetime.date.fromisoformat("1970-01-01")
        intervention.legal.binding_date = datetime.date.fromisoformat("1970-01-01")
        intervention.legal.process_type = KonovaCode.objects.get(id=3)
        intervention.legal.save()
        intervention.legal.laws.set([KonovaCode.objects.get(id=(4))])
        intervention.geometry.geom = self.create_dummy_geometry()
        intervention.geometry.save()
        intervention.save()
        return intervention

    def fill_out_compensation(self, compensation: Compensation) -> Compensation:
        """ Adds all required (dummy) data to a compensation

        Args:
            compensation (Compensation): The compensation which shall be filled out

        Returns:
            compensation (Compensation): The modified compensation
        """
        compensation.after_states.add(self.comp_state)
        compensation.before_states.add(self.comp_state)
        compensation.actions.add(self.comp_action)
        compensation.geometry.geom = self.create_dummy_geometry()
        compensation.deadlines.add(self.finished_deadline)
        compensation.geometry.save()
        return compensation

    def get_conservation_office_code(self):
        """ Returns a dummy KonovaCode as conservation office code

        Returns:

        """
        codelist = KonovaCodeList.objects.get_or_create(
            id=CODELIST_CONSERVATION_OFFICE_ID
        )[0]
        code = KonovaCode.objects.get(id=2)
        codelist.codes.add(code)
        return code

    def get_registration_office_code(self):
        """ Returns a dummy KonovaCode as conservation office code

        Returns:

        """
        codelist = KonovaCodeList.objects.get_or_create(
            id=CODELIST_REGISTRATION_OFFICE_ID
        )[0]
        code = KonovaCode.objects.get(id=3)
        codelist.codes.add(code)
        return code

    def fill_out_ema(self, ema):
        """ Adds all required (dummy) data to an Ema

        Returns:
        """
        ema.responsible.conservation_office = self.get_conservation_office_code()
        ema.responsible.conservation_file_number = "test"
        ema.responsible.handler = self.handler
        ema.responsible.save()
        ema.after_states.add(self.comp_state)
        ema.before_states.add(self.comp_state)
        ema.actions.add(self.comp_action)
        ema.geometry.geom = self.create_dummy_geometry()
        ema.deadlines.add(self.finished_deadline)
        ema.geometry.save()
        return ema

    def fill_out_eco_account(self, eco_account):
        """ Adds all required (dummy) data to an EcoAccount

        Returns:
        """
        eco_account.legal.registration_date = "2022-01-01"
        eco_account.legal.save()
        eco_account.responsible.conservation_office = self.get_conservation_office_code()
        eco_account.responsible.conservation_file_number = "test"
        eco_account.responsible.handler = self.handler
        eco_account.responsible.save()
        eco_account.after_states.add(self.comp_state)
        eco_account.before_states.add(self.comp_state)
        eco_account.actions.add(self.comp_action)
        eco_account.geometry.geom = self.create_dummy_geometry()
        eco_account.geometry.save()
        eco_account.deductable_surface = eco_account.get_surface_after_states()
        eco_account.deadlines.add(self.finished_deadline)
        eco_account.save()
        return eco_account

    def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance = 0.001):
        """ Assert for geometries to be equal

        Transforms the geometries to matching srids before checking

        Args:
            geom1 (MultiPolygon): A geometry
            geom2 (MultiPolygon): A geometry

        Returns:

        """
        # Two empty geometries are basically identical - no further testing
        if geom1.empty and geom2.empty:
            self.assertTrue(True)
            return

        if geom1.srid != geom2.srid:
            # Due to prior possible transformation of any of these geometries, we need to make sure there exists a
            # transformation from one coordinate system into the other, which is valid
            geom1.transform(geom2.srid)
            geom2.transform(geom1.srid)
        self.assertTrue(geom1.equals_exact(geom2, tolerance) or geom2.equals_exact(geom1, tolerance))


class BaseViewTestCase(BaseTestCase):
    """ Wraps basic test functionality, reusable for every specialized ViewTestCase

    """
    login_url = None

    class Meta:
        abstract = True

    @classmethod
    def setUpTestData(cls) -> None:
        super().setUpTestData()
        
    def setUp(self) -> None:
        super().setUp()
        self.login_url = reverse("oauth-login")

    def assert_url_success(self, client: Client, urls: list):
        """ Assert for all given urls a direct 200 response

        Args:
            client (Client): The performing client
            urls (list): An iterable list of urls to be checked

        Returns:

        """
        for url in urls:
            response = client.get(url)
            self.assertEqual(response.status_code, 200, msg=f"Failed for {url}")

    def assert_url_success_redirect(self, client: Client, urls: dict):
        """ Assert for all given urls a 302 response to a certain location.

        Assert the redirect being the expected behaviour.

        Args:
            client (Client): The performing client
            urls (dict): An iterable dict of (urls, redirect_to_url) pairs to be checked

        Returns:

        """
        for url, redirect_to in urls.items():
            response = client.get(url, follow=True)
            # Expect redirects to the landing page
            self.assertEqual(response.redirect_chain[0], (redirect_to, 302), msg=f"Failed for {url}")

    def assert_url_fail(self, client: Client, urls: list):
        """ Assert for all given urls a direct 302 response

        Args:
            client (Client): The performing client
            urls (list): An iterable list of urls to be checked

        Returns:

        """
        for url in urls:
            response = client.get(url)
            self.assertEqual(response.status_code, 302, msg=f"Failed for {url}")


class KonovaViewTestCase(BaseViewTestCase):
    """ Holds tests for all regular views, which are not app specific

    """
    def setUp(self) -> None:
        super().setUp()

        geom = self.create_dummy_geometry()
        self.geom_1 = Geometry.objects.create(
            geom=geom,
        )

        self.home_url = reverse("home")

    def test_views_logged_in_no_groups(self):
        """ Check correct status code for all requests

        Assumption: User logged in but has no groups

        Returns:

        """
        # User logged in
        client = Client()
        client.login(username=self.superuser.username, password=self.superuser_pw)
        self.superuser.groups.set([])
        success_urls = [
            self.home_url
        ]
        self.assert_url_success(client, success_urls)

    def test_views_anonymous_user(self):
        """ Check correct status code for all requests

        Assumption: User logged in but has no groups

        Returns:

        """
        # User not logged in
        client = Client()
        urls = [
            self.home_url
        ]
        self.assert_url_fail(client, urls)

    def test_htmx_parcel_fetch(self):
        """ Tests that the htmx geometry-parcel fetch returns a proper status code and content

        Returns:

        """
        client_user = Client()
        client_user.login(username=self.superuser.username, password=self.superuser_pw)

        has_parcels = self.geom_1.parcels.all().exists()
        if not has_parcels:
            self.geom_1.update_parcels()

        htmx_url = reverse("geometry-parcels", args=(self.geom_1.id,))
        response = client_user.get(htmx_url)
        self.assertEqual(response.status_code, 286, "Unexpected status code for HTMX fetch")
        self.assertGreater(len(response.content), 0)


class AutocompleteTestCase(BaseViewTestCase):
    @classmethod
    def setUpTestData(cls) -> None:
        super().setUpTestData()
        cls.atcmplt_accs = reverse("compensation:acc:autocomplete")
        cls.atcmplt_interventions = reverse("intervention:autocomplete")
        cls.atcmplt_code_comp_action = reverse("codelist:compensation-action-autocomplete")
        cls.atcmplt_code_comp_biotope = reverse("codelist:biotope-autocomplete")
        cls.atcmplt_code_comp_law = reverse("codelist:law-autocomplete")
        cls.atcmplt_code_comp_process = reverse("codelist:process-type-autocomplete")
        cls.atcmplt_code_comp_reg_off = reverse("codelist:registration-office-autocomplete")
        cls.atcmplt_code_comp_cons_off = reverse("codelist:conservation-office-autocomplete")
        cls.atcmplt_code_share_user = reverse("user:share-user-autocomplete")

    def _test_views_anonymous_user(self):
        # ATTENTION: As of the current state of django-autocomplete-light, there is no way to check on authenticated
        # users in a way like @loing_required or anything else. The documentation considers to check on the user's
        # authentication state during get_queryset() of the call. Therefore this test method here will stay here
        # for future clarification but won't be run due to the prefix '_'
        # User not logged in
        client = Client()
        urls = [
            self.atcmplt_accs,
            self.atcmplt_interventions,
            self.atcmplt_code_comp_action,
            self.atcmplt_code_comp_biotope,
            self.atcmplt_code_comp_law,
            self.atcmplt_code_comp_process,
            self.atcmplt_code_comp_reg_off,
            self.atcmplt_code_comp_cons_off,
            self.atcmplt_code_share_user,
        ]
        self.assert_url_fail(client, urls)

    def test_views_logged_in_no_groups(self):
        # User logged in
        client = Client()
        client.login(username=self.superuser.username, password=self.superuser_pw)
        self.superuser.groups.set([])
        urls = [
            self.atcmplt_accs,
            self.atcmplt_interventions,
            self.atcmplt_code_comp_action,
            self.atcmplt_code_comp_biotope,
            self.atcmplt_code_comp_law,
            self.atcmplt_code_comp_process,
            self.atcmplt_code_comp_reg_off,
            self.atcmplt_code_comp_cons_off,
        ]
        self.assert_url_success(client, urls)


class BaseWorkflowTestCase(BaseTestCase):
    """
    Holds base methods and attributes for workflow testing

    """

    client_user = None
    client_anon = None

    class Meta:
        abstract = True

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()

    def assert_object_is_deleted(self, obj):
        """ Provides a quick check whether an object has been removed from the database or not

        Args:
            obj ():

        Returns:

        """
        # Expect the object to be gone from the db
        try:
            obj.refresh_from_db()
            # Well, we should not reach this next line of code, since the object should be gone, therefore not
            # refreshable -> fail!
            self.fail()
        except ObjectDoesNotExist:
            # If we get in here, the test was fine
            pass