from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
from django.contrib.gis.geos import Point
from .models import CustomUser, Geo, ObjItem
from .utils import format_coordinates, parse_pagination_params
from .mixins import RoleRequiredMixin, CoordinateProcessingMixin
from django.views import View


class FormatCoordinatesTestCase(TestCase):
    """Тесты для функции format_coordinates"""

    def test_format_positive_coordinates(self):
        """Тест форматирования положительных координат"""
        result = format_coordinates(37.62, 55.75)
        self.assertEqual(result, "55.75N 37.62E")

    def test_format_negative_longitude(self):
        """Тест форматирования с отрицательной долготой"""
        result = format_coordinates(-122.42, 37.77)
        self.assertEqual(result, "37.77N 122.42W")

    def test_format_negative_latitude(self):
        """Тест форматирования с отрицательной широтой"""
        result = format_coordinates(151.21, -33.87)
        self.assertEqual(result, "33.87S 151.21E")

    def test_format_both_negative(self):
        """Тест форматирования с обеими отрицательными координатами"""
        result = format_coordinates(-58.38, -34.60)
        self.assertEqual(result, "34.6S 58.38W")


class ParsePaginationParamsTestCase(TestCase):
    """Тесты для функции parse_pagination_params"""

    def setUp(self):
        self.factory = RequestFactory()

    def test_default_values(self):
        """Тест значений по умолчанию"""
        request = self.factory.get("/")
        page, per_page = parse_pagination_params(request)
        self.assertEqual(page, 1)
        self.assertEqual(per_page, 50)

    def test_custom_values(self):
        """Тест пользовательских значений"""
        request = self.factory.get("/?page=3&items_per_page=100")
        page, per_page = parse_pagination_params(request)
        self.assertEqual(page, 3)
        self.assertEqual(per_page, 100)

    def test_invalid_page_number(self):
        """Тест невалидного номера страницы"""
        request = self.factory.get("/?page=invalid")
        page, per_page = parse_pagination_params(request)
        self.assertEqual(page, 1)

    def test_negative_page_number(self):
        """Тест отрицательного номера страницы"""
        request = self.factory.get("/?page=-5")
        page, per_page = parse_pagination_params(request)
        self.assertEqual(page, 1)

    def test_max_items_per_page_limit(self):
        """Тест ограничения максимального количества элементов"""
        request = self.factory.get("/?items_per_page=20000")
        page, per_page = parse_pagination_params(request)
        self.assertEqual(per_page, 10000)


class RoleRequiredMixinTestCase(TestCase):
    """Тесты для RoleRequiredMixin"""

    def setUp(self):
        self.factory = RequestFactory()

    def test_admin_has_access(self):
        """Тест что администратор имеет доступ"""
        user = User.objects.create_user(username="testuser", password="12345")
        # Get the automatically created CustomUser and set role to 'admin'
        custom_user = CustomUser.objects.get(user=user)
        custom_user.role = "admin"
        custom_user.save()

        # Refresh user to get updated customuser
        user.refresh_from_db()

        class TestView(RoleRequiredMixin, View):
            required_roles = ["admin", "moderator"]

        view = TestView()
        request = self.factory.get("/")
        request.user = user
        view.request = request

        self.assertTrue(view.test_func())

    def test_user_without_role_denied(self):
        """Тест что пользователь без роли не имеет доступа"""
        user_no_role = User.objects.create_user(username="norole", password="12345")
        # Get the automatically created CustomUser - default role is 'user'
        custom_user_no_role = CustomUser.objects.get(user=user_no_role)
        self.assertEqual(custom_user_no_role.role, "user")

        class TestView(RoleRequiredMixin, View):
            required_roles = ["admin", "moderator"]

        view = TestView()
        request = self.factory.get("/")
        request.user = user_no_role
        view.request = request

        self.assertFalse(view.test_func())


class CoordinateProcessingMixinTestCase(TestCase):
    """Тесты для CoordinateProcessingMixin"""

    def setUp(self):
        self.factory = RequestFactory()

    def test_extract_geo_coordinates(self):
        """Тест извлечения координат геолокации"""

        class TestView(CoordinateProcessingMixin, View):
            pass

        view = TestView()
        request = self.factory.post(
            "/", {"geo_longitude": "37.62", "geo_latitude": "55.75"}
        )
        view.request = request

        coords = view._extract_coordinates("geo")
        self.assertIsNotNone(coords)
        self.assertEqual(coords, (37.62, 55.75))

    def test_extract_invalid_coordinates(self):
        """Тест извлечения невалидных координат"""

        class TestView(CoordinateProcessingMixin, View):
            pass

        view = TestView()
        request = self.factory.post(
            "/", {"geo_longitude": "invalid", "geo_latitude": "55.75"}
        )
        view.request = request

        coords = view._extract_coordinates("geo")
        self.assertIsNone(coords)

    def test_process_coordinates(self):
        """Тест обработки координат и применения к объекту Geo"""

        class TestView(CoordinateProcessingMixin, View):
            pass

        view = TestView()
        request = self.factory.post(
            "/",
            {
                "geo_longitude": "37.62",
                "geo_latitude": "55.75",
            },
        )
        view.request = request

        geo_instance = Geo()
        view.process_coordinates(geo_instance)

        self.assertIsNotNone(geo_instance.coords)
        self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))



class CSVImportTestCase(TestCase):
    """Тесты для функции get_points_from_csv"""

    def setUp(self):
        """Подготовка тестовых данных"""
        from .models import CustomUser, Satellite, Polarization
        from django.contrib.auth.models import User

        # Создаем пользователя
        user = User.objects.create_user(username="testuser", password="12345")
        self.custom_user = CustomUser.objects.get(user=user)

        # Создаем спутник
        self.satellite = Satellite.objects.create(name="Test Satellite", norad=12345)

        # Создаем поляризации
        Polarization.objects.get_or_create(name="Вертикальная")
        Polarization.objects.get_or_create(name="Горизонтальная")
        Polarization.objects.get_or_create(name="Правая")
        Polarization.objects.get_or_create(name="Левая")
        Polarization.objects.get_or_create(name="-")

        # Создаем спутники-зеркала для тестов
        Satellite.objects.get_or_create(name="Mirror1 Satellite", norad=11111)
        Satellite.objects.get_or_create(name="Mirror2 Satellite", norad=22222)
        Satellite.objects.get_or_create(name="Mirror3 Satellite", norad=33333)

    def test_initial_csv_import(self):
        """Тест первичного импорта из CSV файла"""
        from .utils import get_points_from_csv
        from .models import Source, ObjItem

        # Тестовые данные CSV - 3 точки в разных местах
        csv_content = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;Mirror3
2;Signal2 H;55.7560;37.6175;0;01.01.2024 12:05:00;Test Satellite;12345;11520.3;36.0;1;good;Mirror1;Mirror2;
3;Signal3 V;56.8389;60.6057;0;01.01.2024 12:10:00;Test Satellite;12345;11540.7;36.0;1;good;Mirror1;Mirror2;"""

        # Выполняем импорт
        result = get_points_from_csv(csv_content, self.custom_user)

        # Проверяем результаты
        # Первые две точки близко (Москва), третья далеко (Екатеринбург)
        # Должно быть создано 2 источника
        self.assertEqual(result['new_sources'], 2)
        self.assertEqual(result['added'], 3)
        self.assertEqual(result['skipped'], 0)
        self.assertEqual(len(result['errors']), 0)
        self.assertEqual(Source.objects.count(), 2)
        self.assertEqual(ObjItem.objects.count(), 3)

        # Проверяем, что первые две точки привязаны к одному источнику
        source1 = Source.objects.first()
        items_in_source1 = ObjItem.objects.filter(source=source1).count()
        self.assertEqual(items_in_source1, 2)

    def test_csv_import_with_existing_sources(self):
        """Тест импорта CSV с существующими источниками"""
        from .utils import get_points_from_csv
        from .models import Source, ObjItem

        # Первый импорт - создаем начальные данные
        csv_content_1 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;
2;Signal2 H;55.7560;37.6175;0;01.01.2024 12:05:00;Test Satellite;12345;11520.3;36.0;1;good;Mirror1;Mirror2;"""

        result_1 = get_points_from_csv(csv_content_1, self.custom_user)
        self.assertEqual(result_1['new_sources'], 1)
        initial_sources_count = Source.objects.count()
        initial_objitems_count = ObjItem.objects.count()

        # Второй импорт - добавляем новые точки
        # Точка 3 - близко к существующему источнику (Москва)
        # Точка 4 - далеко (Екатеринбург) - создаст новый источник
        csv_content_2 = """3;Signal3 V;55.7562;37.6177;0;01.01.2024 12:10:00;Test Satellite;12345;11540.7;36.0;1;good;Mirror1;Mirror2;
4;Signal4 H;56.8389;60.6057;0;01.01.2024 12:15:00;Test Satellite;12345;11560.2;36.0;1;good;Mirror1;Mirror2;"""

        result_2 = get_points_from_csv(csv_content_2, self.custom_user)

        # Проверяем результаты
        # Должен быть создан 1 новый источник (для точки 4)
        self.assertEqual(result_2['new_sources'], 1)
        self.assertEqual(result_2['added'], 2)
        self.assertEqual(Source.objects.count(), initial_sources_count + 1)
        self.assertEqual(ObjItem.objects.count(), initial_objitems_count + 2)

        # Проверяем, что точка 3 добавлена к существующему источнику
        first_source = Source.objects.first()
        items_in_first_source = ObjItem.objects.filter(source=first_source).count()
        self.assertEqual(items_in_first_source, 3)  # 2 начальных + 1 новая

    def test_csv_import_skip_duplicates(self):
        """Тест пропуска дубликатов при импорте CSV"""
        from .utils import get_points_from_csv
        from .models import Source, ObjItem

        # Первый импорт
        csv_content_1 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;"""

        get_points_from_csv(csv_content_1, self.custom_user)
        initial_sources_count = Source.objects.count()
        initial_objitems_count = ObjItem.objects.count()

        # Второй импорт - та же точка (дубликат)
        csv_content_2 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;"""

        result = get_points_from_csv(csv_content_2, self.custom_user)

        # Проверяем, что дубликат пропущен
        self.assertEqual(result['new_sources'], 0)
        self.assertEqual(result['added'], 0)
        self.assertEqual(result['skipped'], 1)
        self.assertEqual(Source.objects.count(), initial_sources_count)
        self.assertEqual(ObjItem.objects.count(), initial_objitems_count)

    def test_csv_import_mixed_scenario(self):
        """Тест смешанного сценария: дубликаты + новые точки + близкие точки"""
        from .utils import get_points_from_csv
        from .models import Source, ObjItem

        # Первый импорт - 2 точки в Москве
        csv_content_1 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;
2;Signal2 H;55.7560;37.6175;0;01.01.2024 12:05:00;Test Satellite;12345;11520.3;36.0;1;good;Mirror1;Mirror2;"""

        get_points_from_csv(csv_content_1, self.custom_user)

        # Второй импорт:
        # - Точка 1 (дубликат) - должна быть пропущена
        # - Точка 3 (близко к Москве) - должна добавиться к существующему источнику
        # - Точка 4 (Екатеринбург) - должна создать новый источник
        # - Точка 5 (близко к Екатеринбургу) - должна добавиться к новому источнику
        csv_content_2 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;
3;Signal3 V;55.7562;37.6177;0;01.01.2024 12:10:00;Test Satellite;12345;11540.7;36.0;1;good;Mirror1;Mirror2;
4;Signal4 H;56.8389;60.6057;0;01.01.2024 12:15:00;Test Satellite;12345;11560.2;36.0;1;good;Mirror1;Mirror2;
5;Signal5 V;56.8391;60.6059;0;01.01.2024 12:20:00;Test Satellite;12345;11580.8;36.0;1;good;Mirror1;Mirror2;"""

        result = get_points_from_csv(csv_content_2, self.custom_user)

        # Проверяем результаты
        self.assertEqual(result['new_sources'], 1)  # Только для Екатеринбурга
        self.assertEqual(result['added'], 3)  # Точки 3, 4, 5
        self.assertEqual(result['skipped'], 1)  # Точка 1 (дубликат)
        self.assertEqual(Source.objects.count(), 2)  # Москва + Екатеринбург
        self.assertEqual(ObjItem.objects.count(), 5)  # 2 начальных + 3 новых (дубликат пропущен)

        # Проверяем распределение по источникам
        moscow_source = Source.objects.first()
        ekb_source = Source.objects.last()

        moscow_items = ObjItem.objects.filter(source=moscow_source).count()
        ekb_items = ObjItem.objects.filter(source=ekb_source).count()

        self.assertEqual(moscow_items, 3)  # 2 начальных + 1 новая
        self.assertEqual(ekb_items, 2)  # 2 новых точки в Екатеринбурге



class FindMirrorSatellitesTestCase(TestCase):
    """Тесты для функции find_mirror_satellites"""

    def setUp(self):
        """Подготовка тестовых данных"""
        from .models import Satellite

        # Создаем спутники с разными именами
        Satellite.objects.create(name="Eutelsat 16A", norad=40874)
        Satellite.objects.create(name="Eutelsat 21B", norad=41591)
        Satellite.objects.create(name="Astra 4A", norad=41404)
        Satellite.objects.create(name="Turksat 4A", norad=40361)
        Satellite.objects.create(name="Express AM6", norad=39508)

    def test_find_exact_match(self):
        """Тест поиска спутника по точному совпадению"""
        from .utils import find_mirror_satellites

        mirrors = find_mirror_satellites(["Eutelsat 16A"])
        self.assertEqual(len(mirrors), 1)
        self.assertEqual(mirrors[0].name, "Eutelsat 16A")

    def test_find_partial_match(self):
        """Тест поиска спутника по частичному совпадению"""
        from .utils import find_mirror_satellites

        # Ищем по части имени "Eutelsat"
        mirrors = find_mirror_satellites(["eutelsat"])
        self.assertEqual(len(mirrors), 2)
        names = [m.name for m in mirrors]
        self.assertIn("Eutelsat 16A", names)
        self.assertIn("Eutelsat 21B", names)

    def test_find_case_insensitive(self):
        """Тест поиска без учета регистра"""
        from .utils import find_mirror_satellites

        # Разные варианты регистра
        mirrors1 = find_mirror_satellites(["ASTRA"])
        mirrors2 = find_mirror_satellites(["astra"])
        mirrors3 = find_mirror_satellites(["AsTrA"])

        self.assertEqual(len(mirrors1), 1)
        self.assertEqual(len(mirrors2), 1)
        self.assertEqual(len(mirrors3), 1)
        self.assertEqual(mirrors1[0].name, "Astra 4A")
        self.assertEqual(mirrors2[0].name, "Astra 4A")
        self.assertEqual(mirrors3[0].name, "Astra 4A")

    def test_find_multiple_mirrors(self):
        """Тест поиска нескольких зеркал"""
        from .utils import find_mirror_satellites

        mirrors = find_mirror_satellites(["Eutelsat", "Turksat"])
        self.assertEqual(len(mirrors), 3)  # 2 Eutelsat + 1 Turksat
        names = [m.name for m in mirrors]
        self.assertIn("Eutelsat 16A", names)
        self.assertIn("Eutelsat 21B", names)
        self.assertIn("Turksat 4A", names)

    def test_find_with_spaces(self):
        """Тест поиска с пробелами в начале и конце"""
        from .utils import find_mirror_satellites

        mirrors = find_mirror_satellites(["  Express  "])
        self.assertEqual(len(mirrors), 1)
        self.assertEqual(mirrors[0].name, "Express AM6")

    def test_find_empty_list(self):
        """Тест с пустым списком"""
        from .utils import find_mirror_satellites

        mirrors = find_mirror_satellites([])
        self.assertEqual(len(mirrors), 0)

    def test_find_with_dash(self):
        """Тест с дефисом (должен быть пропущен)"""
        from .utils import find_mirror_satellites

        mirrors = find_mirror_satellites(["-"])
        self.assertEqual(len(mirrors), 0)

    def test_find_no_match(self):
        """Тест когда спутник не найден"""
        from .utils import find_mirror_satellites

        mirrors = find_mirror_satellites(["NonExistentSatellite"])
        self.assertEqual(len(mirrors), 0)

    def test_find_removes_duplicates(self):
        """Тест удаления дубликатов"""
        from .utils import find_mirror_satellites

        # Ищем один и тот же спутник дважды
        mirrors = find_mirror_satellites(["Astra", "Astra 4A"])
        self.assertEqual(len(mirrors), 1)
        self.assertEqual(mirrors[0].name, "Astra 4A")
