"""Unit tests for ListingRepository.""" from datetime import datetime, timedelta from unittest.mock import AsyncMock, MagicMock import pytest from sqlalchemy import Engine from models.listing import ( FurnishType, ListingType, QueryParameters, RentListing, ) from repositories.listing_repository import ListingRepository class TestListingRepository: """Tests for ListingRepository methods.""" async def test_get_listings_empty_db( self, listing_repository: ListingRepository ) -> None: """Test that get_listings returns empty list for empty database.""" listings = await listing_repository.get_listings() assert listings == [] async def test_get_listings_returns_inserted_listings( self, listing_repository: ListingRepository, sample_rent_listing: RentListing, ) -> None: """Test that get_listings returns listings that were inserted.""" await listing_repository.upsert_listings([sample_rent_listing]) listings = await listing_repository.get_listings() assert len(listings) == 1 assert listings[0].id == sample_rent_listing.id async def test_upsert_listings_creates_new( self, listing_repository: ListingRepository, sample_rent_listing: RentListing, ) -> None: """Test that upsert_listings creates new listings.""" result = await listing_repository.upsert_listings([sample_rent_listing]) assert len(result) == 1 assert result[0].id == sample_rent_listing.id # Verify it's in the database listings = await listing_repository.get_listings() assert len(listings) == 1 async def test_upsert_listings_updates_existing( self, listing_repository: ListingRepository, sample_rent_listing: RentListing, ) -> None: """Test that upsert_listings updates existing listings.""" # Insert initial listing await listing_repository.upsert_listings([sample_rent_listing]) # Update the listing sample_rent_listing.price = 3000.0 await listing_repository.upsert_listings([sample_rent_listing]) # Verify update listings = await listing_repository.get_listings() assert len(listings) == 1 assert listings[0].price == 3000.0 async def test_mark_seen_updates_timestamp( self, listing_repository: ListingRepository, sample_rent_listing: RentListing, ) -> None: """Test that mark_seen updates the last_seen timestamp.""" # Set an old timestamp old_time = datetime.now() - timedelta(days=7) sample_rent_listing.last_seen = old_time await listing_repository.upsert_listings([sample_rent_listing]) # Mark as seen await listing_repository.mark_seen(sample_rent_listing.id) # Verify timestamp was updated listings = await listing_repository.get_listings() assert len(listings) == 1 assert listings[0].last_seen > old_time async def test_mark_seen_nonexistent_listing( self, listing_repository: ListingRepository ) -> None: """Test that mark_seen handles nonexistent listings gracefully.""" # Should not raise an exception await listing_repository.mark_seen(999999) async def test_get_listings_with_only_ids( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test that get_listings filters by only_ids.""" await listing_repository.upsert_listings(sample_rent_listings) # Request only specific IDs listings = await listing_repository.get_listings(only_ids=[1, 3]) assert len(listings) == 2 listing_ids = [l.id for l in listings] assert 1 in listing_ids assert 3 in listing_ids assert 2 not in listing_ids async def test_get_listings_with_limit( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test that get_listings respects limit parameter.""" await listing_repository.upsert_listings(sample_rent_listings) listings = await listing_repository.get_listings(limit=2) assert len(listings) == 2 class TestListingRepositoryFilters: """Tests for ListingRepository query parameter filtering.""" async def test_filter_by_bedrooms( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test filtering by bedroom count.""" await listing_repository.upsert_listings(sample_rent_listings) query_params = QueryParameters( listing_type=ListingType.RENT, min_bedrooms=2, max_bedrooms=2, ) listings = await listing_repository.get_listings(query_parameters=query_params) assert len(listings) == 1 assert listings[0].number_of_bedrooms == 2 async def test_filter_by_price_range( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test filtering by price range.""" await listing_repository.upsert_listings(sample_rent_listings) query_params = QueryParameters( listing_type=ListingType.RENT, min_price=1800, max_price=2500, ) listings = await listing_repository.get_listings(query_parameters=query_params) assert len(listings) == 1 assert listings[0].price == 2000.0 async def test_filter_by_min_sqm( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test filtering by minimum square meters.""" await listing_repository.upsert_listings(sample_rent_listings) query_params = QueryParameters( listing_type=ListingType.RENT, min_sqm=60, ) listings = await listing_repository.get_listings(query_parameters=query_params) assert len(listings) == 1 assert listings[0].square_meters == 80.0 async def test_filter_by_furnish_type( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test filtering by furnish type.""" await listing_repository.upsert_listings(sample_rent_listings) query_params = QueryParameters( listing_type=ListingType.RENT, furnish_types=[FurnishType.UNFURNISHED], ) listings = await listing_repository.get_listings(query_parameters=query_params) assert len(listings) == 1 assert listings[0].furnish_type == FurnishType.UNFURNISHED async def test_filter_by_last_seen_days( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test filtering by last_seen_days.""" # Make one listing old sample_rent_listings[0].last_seen = datetime.now() - timedelta(days=30) await listing_repository.upsert_listings(sample_rent_listings) query_params = QueryParameters( listing_type=ListingType.RENT, last_seen_days=7, ) listings = await listing_repository.get_listings(query_parameters=query_params) # Only 2 should be recent enough assert len(listings) == 2 async def test_combined_filters( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test combining multiple filters.""" await listing_repository.upsert_listings(sample_rent_listings) query_params = QueryParameters( listing_type=ListingType.RENT, min_bedrooms=1, max_bedrooms=2, min_price=1000, max_price=2500, furnish_types=[FurnishType.FURNISHED, FurnishType.UNFURNISHED], ) listings = await listing_repository.get_listings(query_parameters=query_params) # Should match listings with 1-2 bedrooms in price range assert len(listings) == 2 class TestListingRepositoryStreaming: """Tests for streaming and optimized query methods.""" async def test_count_listings_empty_db( self, listing_repository: ListingRepository ) -> None: """Test count returns 0 for empty database.""" count = listing_repository.count_listings() assert count == 0 async def test_count_listings_with_data( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test count returns correct number.""" await listing_repository.upsert_listings(sample_rent_listings) count = listing_repository.count_listings() assert count == 3 async def test_count_listings_with_filters( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test count respects query parameters.""" await listing_repository.upsert_listings(sample_rent_listings) query_params = QueryParameters( listing_type=ListingType.RENT, min_bedrooms=2, max_bedrooms=3, ) count = listing_repository.count_listings(query_parameters=query_params) assert count == 2 async def test_stream_listings_optimized_returns_dicts( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test optimized streaming returns dict rows.""" await listing_repository.upsert_listings(sample_rent_listings) results = list(listing_repository.stream_listings_optimized()) assert len(results) == 3 # Each result should be a dict for row in results: assert isinstance(row, dict) assert "id" in row assert "price" in row assert "number_of_bedrooms" in row async def test_stream_listings_optimized_respects_limit( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test streaming limit parameter.""" await listing_repository.upsert_listings(sample_rent_listings) results = list(listing_repository.stream_listings_optimized(limit=2)) assert len(results) == 2 async def test_get_listing_ids( self, listing_repository: ListingRepository, sample_rent_listings: list[RentListing], ) -> None: """Test get_listing_ids returns set of IDs.""" await listing_repository.upsert_listings(sample_rent_listings) ids = listing_repository.get_listing_ids() assert isinstance(ids, set) assert ids == {1, 2, 3} async def test_get_listing_ids_empty_db( self, listing_repository: ListingRepository, ) -> None: """Test get_listing_ids returns empty set for empty database.""" ids = listing_repository.get_listing_ids() assert isinstance(ids, set) assert len(ids) == 0 class TestFurnishTypeParsing: """Tests for _parse_furnish_type helper.""" def test_parse_furnish_type_none_detailobject(self) -> None: """Test that None detailobject returns UNKNOWN.""" listing = MagicMock() listing.detailobject = None result = ListingRepository._parse_furnish_type(listing) assert result == FurnishType.UNKNOWN def test_parse_furnish_type_missing_property_key(self) -> None: """Test that missing 'property' key returns UNKNOWN.""" listing = MagicMock() listing.detailobject = {} result = ListingRepository._parse_furnish_type(listing) assert result == FurnishType.UNKNOWN def test_parse_furnish_type_missing_let_furnish_type(self) -> None: """Test that missing 'letFurnishType' key returns UNKNOWN.""" listing = MagicMock() listing.detailobject = {"property": {}} result = ListingRepository._parse_furnish_type(listing) assert result == FurnishType.UNKNOWN def test_parse_furnish_type_null_value(self) -> None: """Test that null letFurnishType value returns UNKNOWN.""" listing = MagicMock() listing.detailobject = {"property": {"letFurnishType": None}} result = ListingRepository._parse_furnish_type(listing) assert result == FurnishType.UNKNOWN def test_parse_furnish_type_furnished(self) -> None: """Test that 'Furnished' is parsed correctly.""" listing = MagicMock() listing.detailobject = {"property": {"letFurnishType": "Furnished"}} result = ListingRepository._parse_furnish_type(listing) assert result == FurnishType.FURNISHED def test_parse_furnish_type_unfurnished(self) -> None: """Test that 'Unfurnished' is parsed correctly.""" listing = MagicMock() listing.detailobject = {"property": {"letFurnishType": "Unfurnished"}} result = ListingRepository._parse_furnish_type(listing) assert result == FurnishType.UNFURNISHED def test_parse_furnish_type_part_furnished(self) -> None: """Test that 'Part Furnished' is parsed correctly.""" listing = MagicMock() listing.detailobject = {"property": {"letFurnishType": "Part Furnished"}} result = ListingRepository._parse_furnish_type(listing) assert result == FurnishType.PART_FURNISHED def test_parse_furnish_type_landlord_variant(self) -> None: """Test that landlord variants map to ASK_LANDLORD.""" listing = MagicMock() listing.detailobject = {"property": {"letFurnishType": "Ask Landlord Please"}} result = ListingRepository._parse_furnish_type(listing) assert result == FurnishType.ASK_LANDLORD def test_parse_furnish_type_landlord_case_insensitive(self) -> None: """Test that landlord check is case-insensitive.""" listing = MagicMock() listing.detailobject = {"property": {"letFurnishType": "LANDLORD decides"}} result = ListingRepository._parse_furnish_type(listing) assert result == FurnishType.ASK_LANDLORD