"""
Test utilities and helpers following Django styleguide patterns.
Provides reusable testing patterns and assertion helpers.
"""

from datetime import date, datetime
from typing import Any

from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APITestCase

User = get_user_model()


class ApiTestMixin:
    """Mixin providing common API testing utilities."""

    def assertApiResponse(
        self,
        response,
        *,
        status_code: int = status.HTTP_200_OK,
        response_status: str = "success",
        data_type: type | None = None,
        contains_fields: list[str] | None = None,
    ):
        """
        Assert API response has correct structure and content.

        Args:
            response: DRF Response object
            status_code: Expected HTTP status code
            response_status: Expected response status ('success' or 'error')
            data_type: Expected type of response data (list, dict, etc.)
            contains_fields: List of fields that should be in response data
        """
        self.assertEqual(response.status_code, status_code)
        self.assertEqual(response.data["status"], response_status)

        if response_status == "success":
            self.assertIn("data", response.data)

            if data_type:
                self.assertIsInstance(response.data["data"], data_type)

            if contains_fields and response.data["data"]:
                data = response.data["data"]
                # Handle both single objects and lists
                if isinstance(data, list) and data:
                    data = data[0]

                if isinstance(data, dict):
                    for field in contains_fields:
                        self.assertIn(
                            field,
                            data,
                            f"Field '{field}' missing from response data",
                        )

        elif response_status == "error":
            self.assertIn("error", response.data)
            self.assertIn("code", response.data["error"])
            self.assertIn("message", response.data["error"])

    def assertApiError(
        self,
        response,
        *,
        status_code: int,
        error_code: str | None = None,
        message_contains: str | None = None,
    ):
        """
        Assert API response is an error with specific characteristics.

        Args:
            response: DRF Response object
            status_code: Expected HTTP status code
            error_code: Expected error code in response
            message_contains: String that should be in error message
        """
        self.assertApiResponse(response, status_code=status_code, response_status="error")

        if error_code:
            self.assertEqual(response.data["error"]["code"], error_code)

        if message_contains:
            self.assertIn(message_contains, response.data["error"]["message"])

    def assertPaginatedResponse(
        self,
        response,
        *,
        expected_count: int | None = None,
        has_next: bool | None = None,
        has_previous: bool | None = None,
    ):
        """
        Assert API response has correct pagination structure.

        Args:
            response: DRF Response object
            expected_count: Expected number of items in current page
            has_next: Whether pagination should have next page
            has_previous: Whether pagination should have previous page
        """
        self.assertApiResponse(response, data_type=list)
        self.assertIn("pagination", response.data)

        pagination = response.data["pagination"]
        required_fields = [
            "page",
            "page_size",
            "total_pages",
            "total_count",
            "has_next",
            "has_previous",
        ]

        for field in required_fields:
            self.assertIn(field, pagination)

        if expected_count is not None:
            self.assertEqual(len(response.data["data"]), expected_count)

        if has_next is not None:
            self.assertEqual(pagination["has_next"], has_next)

        if has_previous is not None:
            self.assertEqual(pagination["has_previous"], has_previous)


class ModelTestMixin:
    """Mixin providing common model testing utilities."""

    def assertModelFields(self, instance, expected_fields: dict[str, Any]):
        """
        Assert model instance has expected field values.

        Args:
            instance: Model instance
            expected_fields: Dict of field_name: expected_value
        """
        for field_name, expected_value in expected_fields.items():
            actual_value = getattr(instance, field_name)
            self.assertEqual(
                actual_value,
                expected_value,
                f"Field '{field_name}' expected {expected_value}, got {actual_value}",
            )

    def assertModelValidation(
        self,
        model_class,
        invalid_data: dict[str, Any],
        expected_errors: list[str],
    ):
        """
        Assert model validation catches expected errors.

        Args:
            model_class: Model class to test
            invalid_data: Data that should cause validation errors
            expected_errors: List of error messages that should be raised
        """
        instance = model_class(**invalid_data)

        with self.assertRaises(Exception) as context:
            instance.full_clean()

        exception_str = str(context.exception)
        for expected_error in expected_errors:
            self.assertIn(expected_error, exception_str)

    def assertDatabaseConstraint(self, model_factory, invalid_data: dict[str, Any]):
        """
        Assert database constraint is enforced.

        Args:
            model_factory: Factory class for creating model instances
            invalid_data: Data that should violate database constraints
        """
        from django.db import IntegrityError

        with self.assertRaises(IntegrityError):
            model_factory(**invalid_data)


class FactoryTestMixin:
    """Mixin providing factory testing utilities."""

    def assertFactoryCreatesValidInstance(self, factory_class, **kwargs):
        """
        Assert factory creates valid model instance.

        Args:
            factory_class: Factory class to test
            **kwargs: Additional factory parameters
        """
        instance = factory_class(**kwargs)

        # Basic assertions
        self.assertIsNotNone(instance.id)
        self.assertIsNotNone(instance.created_at)

        # Run full_clean to ensure validity
        instance.full_clean()

        return instance

    def assertFactoryBatchCreation(self, factory_class, count: int = 5, **kwargs):
        """
        Assert factory can create multiple valid instances.

        Args:
            factory_class: Factory class to test
            count: Number of instances to create
            **kwargs: Additional factory parameters
        """
        instances = factory_class.create_batch(count, **kwargs)

        self.assertEqual(len(instances), count)

        for instance in instances:
            self.assertIsNotNone(instance.id)
            instance.full_clean()

        return instances


class TimestampTestMixin:
    """Mixin for testing timestamp-related functionality."""

    def assertRecentTimestamp(self, timestamp, tolerance_seconds: int = 5):
        """
        Assert timestamp is recent (within tolerance).

        Args:
            timestamp: Timestamp to check
            tolerance_seconds: Allowed difference in seconds
        """
        from django.utils import timezone

        now = timezone.now()
        if isinstance(timestamp, date) and not isinstance(timestamp, datetime):
            # Convert date to datetime for comparison
            timestamp = datetime.combine(timestamp, datetime.min.time())
            timestamp = timezone.make_aware(timestamp)

        time_diff = abs((now - timestamp).total_seconds())
        self.assertLessEqual(
            time_diff,
            tolerance_seconds,
            f"Timestamp {timestamp} is not recent (diff: {time_diff}s)",
        )

    def assertTimestampOrder(self, earlier_timestamp, later_timestamp):
        """
        Assert timestamps are in correct order.

        Args:
            earlier_timestamp: Should be before later_timestamp
            later_timestamp: Should be after earlier_timestamp
        """
        self.assertLess(
            earlier_timestamp,
            later_timestamp,
            f"Timestamps not in order: {earlier_timestamp} should be before {later_timestamp}",
        )


class GeographyTestMixin:
    """Mixin for testing geography-related functionality."""

    def assertValidCoordinates(self, latitude: float, longitude: float):
        """
        Assert coordinates are within valid ranges.

        Args:
            latitude: Latitude value
            longitude: Longitude value
        """
        self.assertGreaterEqual(latitude, -90, "Latitude below valid range")
        self.assertLessEqual(latitude, 90, "Latitude above valid range")
        self.assertGreaterEqual(longitude, -180, "Longitude below valid range")
        self.assertLessEqual(longitude, 180, "Longitude above valid range")

    def assertCoordinateDistance(self, point1: tuple, point2: tuple, max_distance_km: float):
        """
        Assert two geographic points are within specified distance.

        Args:
            point1: (latitude, longitude) tuple
            point2: (latitude, longitude) tuple
            max_distance_km: Maximum allowed distance in kilometers
        """
        from math import asin, cos, radians, sin, sqrt

        lat1, lon1 = point1
        lat2, lon2 = point2

        # Haversine formula for great circle distance
        lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
        dlat = lat2 - lat1
        dlon = lon2 - lon1
        a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
        c = 2 * asin(sqrt(a))
        distance_km = 6371 * c  # Earth's radius in km

        self.assertLessEqual(
            distance_km,
            max_distance_km,
            f"Points are {distance_km:.2f}km apart, exceeds {max_distance_km}km",
        )


class EnhancedTestCase(
    ApiTestMixin,
    ModelTestMixin,
    FactoryTestMixin,
    TimestampTestMixin,
    GeographyTestMixin,
    TestCase,
):
    """Enhanced TestCase with all testing mixins."""


class EnhancedAPITestCase(
    ApiTestMixin,
    ModelTestMixin,
    FactoryTestMixin,
    TimestampTestMixin,
    GeographyTestMixin,
    APITestCase,
):
    """Enhanced APITestCase with all testing mixins."""
