from django.test import TestCase
from django.utils import timezone
from django.core.exceptions import FieldDoesNotExist

from api.models import Photo, AlbumDate
from api.models.photo_caption import PhotoCaption
from api.models.photo_search import PhotoSearch
from api.tests.utils import create_test_user, create_test_photo


class PhotoModelIntegrationTest(TestCase):
    def setUp(self):
        self.user = create_test_user()
        self.photo = create_test_photo(owner=self.user)

    def test_photo_properties_delegate_to_caption_model(self):
        """Test that Photo properties delegate to PhotoCaption model"""
        # Initially no caption instance

        caption_instance, created = PhotoCaption.objects.get_or_create(photo=self.photo)
        self.assertIsNone(caption_instance.captions_json)

        # Setting captions_json should create PhotoCaption instance
        caption_instance.captions_json = {"user_caption": "Test caption"}
        caption_instance.save()

        # Verify PhotoCaption was created
        self.assertTrue(PhotoCaption.objects.filter(photo=self.photo).exists())
        caption = PhotoCaption.objects.get(photo=self.photo)
        self.assertEqual(caption.captions_json["user_caption"], "Test caption")

        # Verify property returns the value
        self.assertEqual(caption.captions_json["user_caption"], "Test caption")

    def test_photo_properties_delegate_to_search_model(self):
        """Test that Photo properties delegate to PhotoSearch model"""
        # Initially no search instance

        search_instance, created = PhotoSearch.objects.get_or_create(photo=self.photo)
        self.assertIsNone(search_instance.search_captions)
        self.assertIsNone(search_instance.search_location)

        # Setting search properties should create PhotoSearch instance
        search_instance.search_captions = "outdoor nature"
        search_instance.search_location = "New York"
        search_instance.save()

        # Verify PhotoSearch was created
        self.assertTrue(PhotoSearch.objects.filter(photo=self.photo).exists())
        search = PhotoSearch.objects.get(photo=self.photo)
        self.assertEqual(search.search_captions, "outdoor nature")
        self.assertEqual(search.search_location, "New York")

        # Refresh photo instance to get updated properties
        self.photo.refresh_from_db()

        # Verify properties return the values through direct access
        self.assertEqual(search_instance.search_captions, "outdoor nature")
        self.assertEqual(search_instance.search_location, "New York")

    def test_photo_caption_methods_work_directly(self):
        """Test that PhotoCaption methods work directly with the photo"""

        # Test save_user_caption method directly on PhotoCaption
        caption_instance, created = PhotoCaption.objects.get_or_create(photo=self.photo)
        result = caption_instance.save_user_caption(caption="My test caption")

        # The method succeeds because create_test_photo creates a Thumbnail
        # with a thumbnail_big path (even though the file doesn't exist on disk)
        self.assertTrue(result)
        self.assertTrue(PhotoCaption.objects.filter(photo=self.photo).exists())

    def test_photo_search_methods_work_directly(self):
        """Test that PhotoSearch methods work directly with the photo"""
        # Create some caption data first

        caption_instance, created = PhotoCaption.objects.get_or_create(photo=self.photo)
        caption_instance.captions_json = {"user_caption": "Beautiful landscape"}
        caption_instance.save()

        # Test recreate_search_captions method directly on PhotoSearch
        search_instance, created = PhotoSearch.objects.get_or_create(photo=self.photo)
        search_instance.recreate_search_captions()
        search_instance.save()

        # Verify PhotoSearch was created and search captions updated
        self.assertTrue(PhotoSearch.objects.filter(photo=self.photo).exists())
        search = PhotoSearch.objects.get(photo=self.photo)
        self.assertIn("Beautiful landscape", search.search_captions)

    def test_geolocate_updates_search_location(self):
        """Test that _geolocate method updates search location"""
        # Mock geolocation data
        geolocation_data = {
            "features": [
                {"text": "Central Park"},
                {"text": "New York"},
                {"text": "USA"},
            ]
        }

        # Set geolocation_json on photo
        self.photo.geolocation_json = geolocation_data
        self.photo.save()

        # Manually trigger the search location update part

        search_instance, created = PhotoSearch.objects.get_or_create(photo=self.photo)
        search_instance.update_search_location(geolocation_data)
        search_instance.save()

        # Refresh to get updated data
        self.photo.refresh_from_db()

        # Verify search location was updated
        self.assertIn("Central Park", search_instance.search_location)
        self.assertIn("New York", search_instance.search_location)
        self.assertIn("USA", search_instance.search_location)

    def test_cascade_deletion_of_related_models(self):
        """Test that deleting Photo cascades to PhotoCaption and PhotoSearch"""
        # Create related instances

        caption_instance, created = PhotoCaption.objects.get_or_create(photo=self.photo)
        caption_instance.captions_json = {"user_caption": "Test"}
        caption_instance.save()

        search_instance, created = PhotoSearch.objects.get_or_create(photo=self.photo)
        search_instance.search_captions = "Test"
        search_instance.save()

        # Verify instances exist
        self.assertTrue(PhotoCaption.objects.filter(photo=self.photo).exists())
        self.assertTrue(PhotoSearch.objects.filter(photo=self.photo).exists())

        photo_id = self.photo.image_hash
        self.photo.delete()

        # Verify instances are deleted when photo is deleted
        self.assertFalse(PhotoCaption.objects.filter(photo_id=photo_id).exists())
        self.assertFalse(PhotoSearch.objects.filter(photo_id=photo_id).exists())

    def test_lazy_creation_of_related_instances(self):
        """Test that related instances are created only when needed"""

        # Initially no instances should exist
        self.assertFalse(PhotoCaption.objects.filter(photo=self.photo).exists())
        self.assertFalse(PhotoSearch.objects.filter(photo=self.photo).exists())

        # Calling direct access methods should create instances
        caption_instance, created = PhotoCaption.objects.get_or_create(photo=self.photo)
        search_instance, created = PhotoSearch.objects.get_or_create(photo=self.photo)

        self.assertIsNone(caption_instance.captions_json)
        self.assertIsNone(search_instance.search_captions)
        self.assertIsNone(search_instance.search_location)

        # Now instances should exist
        self.assertTrue(PhotoCaption.objects.filter(photo=self.photo).exists())
        self.assertTrue(PhotoSearch.objects.filter(photo=self.photo).exists())

        # Setting properties should save data to instances
        caption_instance.captions_json = {"test": "value"}
        search_instance.search_captions = "test"
        caption_instance.save()
        search_instance.save()

        # Verify data is stored
        self.assertTrue(PhotoCaption.objects.filter(photo=self.photo).exists())
        self.assertTrue(PhotoSearch.objects.filter(photo=self.photo).exists())

    def test_get_or_create_methods(self):
        """Test the _get_or_create_* methods"""
        # Test caption instance creation

        caption_instance1, created = PhotoCaption.objects.get_or_create(
            photo=self.photo
        )
        caption_instance2, created = PhotoCaption.objects.get_or_create(
            photo=self.photo
        )

        # Should return the same instance (using photo_id as primary key)
        self.assertEqual(caption_instance1.photo_id, caption_instance2.photo_id)

        # Test search instance creation

        search_instance1, created = PhotoSearch.objects.get_or_create(photo=self.photo)
        search_instance2, created = PhotoSearch.objects.get_or_create(photo=self.photo)

        # Should return the same instance (using photo_id as primary key)
        self.assertEqual(search_instance1.photo_id, search_instance2.photo_id)

    def test_complex_workflow(self):
        """Test a complex workflow involving all models"""
        # 1. Add user caption (will fail due to thumbnail but creates instance)

        caption_instance, created = PhotoCaption.objects.get_or_create(photo=self.photo)
        caption_instance.save_user_caption(caption="My vacation photo")

        # 2. Add places365 data directly
        caption_instance.captions_json = {
            "user_caption": "My vacation photo",
            "places365": {
                "categories": ["outdoor", "beach"],
                "attributes": ["sunny", "natural"],
                "environment": "outdoor",
            },
        }
        caption_instance.save()

        # 3. Recreate search captions

        search_instance, created = PhotoSearch.objects.get_or_create(photo=self.photo)
        search_instance.recreate_search_captions()

        # 4. Add geolocation
        geolocation_data = {
            "features": [{"text": "Miami Beach"}, {"text": "Florida"}, {"text": "USA"}]
        }
        self.photo.geolocation_json = geolocation_data
        search_instance.update_search_location(geolocation_data)
        search_instance.save()

        # Refresh to get updated data
        self.photo.refresh_from_db()

        # Verify final state
        self.assertEqual(
            caption_instance.captions_json["user_caption"], "My vacation photo"
        )
        self.assertIn("outdoor", search_instance.search_captions)
        self.assertIn("beach", search_instance.search_captions)
        self.assertIn("My vacation photo", search_instance.search_captions)
        self.assertIn("Miami Beach", search_instance.search_location)

    def test_property_error_handling(self):
        """Test error handling in property getters"""
        # Test when related instances don't exist and there's an error
        photo = create_test_photo(owner=self.user)

        # These should return None gracefully, not raise exceptions

        caption_instance, created = PhotoCaption.objects.get_or_create(photo=photo)
        search_instance, created = PhotoSearch.objects.get_or_create(photo=photo)

        self.assertIsNone(caption_instance.captions_json)
        self.assertIsNone(search_instance.search_captions)
        self.assertIsNone(search_instance.search_location)

    def test_backward_compatibility(self):
        """Test that the refactored models maintain backward compatibility"""
        # Create instances using the refactored approach

        caption_instance, created = PhotoCaption.objects.get_or_create(photo=self.photo)
        caption_instance.captions_json = {
            "user_caption": "Test caption",
            "im2txt": "Generated caption",
        }
        caption_instance.save()

        search_instance, created = PhotoSearch.objects.get_or_create(photo=self.photo)
        search_instance.search_captions = "test search terms"
        search_instance.search_location = "Test Location"
        search_instance.save()

        # Refresh to get updated data
        self.photo.refresh_from_db()

        # Verify data is accessible through properties
        self.assertEqual(caption_instance.captions_json["user_caption"], "Test caption")
        self.assertEqual(caption_instance.captions_json["im2txt"], "Generated caption")
        self.assertEqual(search_instance.search_captions, "test search terms")
        self.assertEqual(search_instance.search_location, "Test Location")

        # Verify data is stored in the correct models
        caption = PhotoCaption.objects.get(photo=self.photo)
        search = PhotoSearch.objects.get(photo=self.photo)

        self.assertEqual(caption.captions_json["user_caption"], "Test caption")
        self.assertEqual(search.search_captions, "test search terms")
        self.assertEqual(search.search_location, "Test Location")

    def test_queryset_only_with_search_location_fails(self):
        """Test that using search_location in queryset .only() raises FieldDoesNotExist"""
        # This should fail because search_location is no longer a database field
        with self.assertRaises(FieldDoesNotExist):
            list(Photo.objects.only("image_hash", "search_location").all())

    def test_queryset_only_with_search_instance_works(self):
        """Test that using search_instance__search_location in queryset .only() works"""
        # Create a photo with search data

        search_instance, created = PhotoSearch.objects.get_or_create(photo=self.photo)
        search_instance.search_location = "New York"
        search_instance.save()

        # This should work because we're accessing through the related model
        photos = list(
            Photo.objects.select_related("search_instance")
            .only("image_hash", "search_instance__search_location")
            .filter(image_hash=self.photo.image_hash)
        )

        # Should be able to access the data
        self.assertEqual(len(photos), 1)
        # Note: accessing search_location property will still work even with .only()
        # because it goes through the related model

    def test_album_date_queryset_works(self):
        """Test that the album date queryset works with the fixed field references"""

        # Create a photo with search data

        search_instance, created = PhotoSearch.objects.get_or_create(photo=self.photo)
        search_instance.search_location = "New York"
        search_instance.search_captions = "outdoor nature"
        search_instance.save()

        # Create an album date and add the photo to it
        album_date = AlbumDate.objects.create(
            date=self.photo.exif_timestamp.date()
            if self.photo.exif_timestamp
            else timezone.now().date(),
            owner=self.user,
        )
        album_date.photos.add(self.photo)

        # This should work with the corrected field references
        photo_qs = (
            album_date.photos.all()
            .select_related("search_instance")
            .only(
                "image_hash",
                "search_instance__search_location",
                "exif_timestamp",
            )
        )

        photos = list(photo_qs)
        self.assertEqual(len(photos), 1)
        self.assertEqual(photos[0].search_instance.search_location, "New York")


class AddLocationToAlbumDatesTest(TestCase):
    """Regression tests for Photo._add_location_to_album_dates (issue #1268).

    The function reaches into geolocation_json["places"][-2] to pick a city name.
    Real-world data is messy: photos may have no geolocation, a partial geocode
    result, only a country-level place, or stale-format JSON missing the
    "places" key entirely. None of those should crash the rescan job.
    """

    def setUp(self):
        self.user = create_test_user()
        self.photo = create_test_photo(owner=self.user, exif_timestamp=timezone.now())

    def test_does_not_crash_when_geolocation_json_is_none(self):
        self.photo.geolocation_json = None
        self.photo._add_location_to_album_dates()

    def test_does_not_crash_when_geolocation_json_is_empty_dict(self):
        self.photo.geolocation_json = {}
        self.photo._add_location_to_album_dates()

    def test_does_not_crash_when_places_key_is_missing(self):
        # Legacy-format records seen in long-lived databases (the schema
        # carried a top-level "city" before commit 134e8847 in 2023).
        self.photo.geolocation_json = {"city": "Berlin", "_v": "1"}
        self.photo._add_location_to_album_dates()

    def test_does_not_crash_when_places_is_empty(self):
        self.photo.geolocation_json = {"places": []}
        self.photo._add_location_to_album_dates()

    def test_does_not_crash_when_places_has_one_entry(self):
        # Remote locations where the geocoder only resolves the country.
        self.photo.geolocation_json = {"places": ["Antarctica"]}
        self.photo._add_location_to_album_dates()

    def test_writes_city_to_album_date_location(self):
        album_date = AlbumDate.objects.create(
            date=self.photo.exif_timestamp.date(), owner=self.user
        )
        album_date.photos.add(self.photo)

        self.photo.geolocation_json = {"places": ["Berlin", "Germany"]}
        self.photo._add_location_to_album_dates()

        album_date.refresh_from_db()
        self.assertEqual(album_date.location, {"places": ["Berlin"]})
