# frozen_string_literal: true require 'rails_helper' RSpec.describe Visits::PlaceFinder do let(:user) { create(:user) } let(:latitude) { 40.7128 } let(:longitude) { -74.0060 } subject { described_class.new(user) } describe '#find_or_create_place' do let(:visit_data) do { center_lat: latitude, center_lon: longitude, suggested_name: 'Test Place', points: [] } end context 'when an existing place is found' do let!(:existing_place) { create(:place, latitude: latitude, longitude: longitude) } it 'returns the existing place as main_place' do result = subject.find_or_create_place(visit_data) expect(result).to be_a(Hash) expect(result[:main_place]).to eq(existing_place) end it 'includes suggested places in the result' do result = subject.find_or_create_place(visit_data) expect(result[:suggested_places]).to be_an(Array) expect(result[:suggested_places]).to include(existing_place) end it 'finds an existing place by name within search radius' do similar_named_place = create(:place, name: 'Test Place', latitude: latitude + 0.0001, longitude: longitude + 0.0001) # Use the name but slightly different coordinates modified_visit_data = visit_data.merge( center_lat: latitude + 0.0002, center_lon: longitude + 0.0002 ) result = subject.find_or_create_place(modified_visit_data) expect(result[:main_place]).to eq(similar_named_place) end end context 'with places from points data' do let(:point_with_geodata) do build_stubbed(:point, latitude: latitude, longitude: longitude, geodata: { 'properties' => { 'name' => 'POI from Point', 'city' => 'New York', 'country' => 'USA' } }) end let(:visit_data_with_points) do visit_data.merge(points: [point_with_geodata]) end before do # Mock external API calls to isolate point-based place creation allow(Geocoder).to receive(:search).and_return([]) allow(subject).to receive(:fetch_places_from_api).and_return([]) end it 'extracts and creates places from point geodata' do allow(subject).to receive(:create_place_from_point).and_call_original expect do result = subject.find_or_create_place(visit_data_with_points) expect(result[:main_place].name).to include('POI from Point') end.to change(Place, :count).by(1) expect(subject).to have_received(:create_place_from_point) end end context 'when no existing place is found' do let(:geocoder_result) do double( data: { 'properties' => { 'name' => 'Test Location', 'street' => 'Test Street', 'city' => 'Test City', 'country' => 'Test Country' } }, latitude: latitude, longitude: longitude ) end let(:other_geocoder_result) do double( data: { 'properties' => { 'name' => 'Other Location', 'street' => 'Other Street', 'city' => 'Test City', 'country' => 'Test Country' } }, latitude: latitude + 0.001, longitude: longitude + 0.001 ) end before do allow(Geocoder).to receive(:search).and_return([geocoder_result, other_geocoder_result]) end it 'creates a new place with geocoded data' do # Main place and other place expect do result = subject.find_or_create_place(visit_data) expect(result[:main_place].name).to include('Test Location') end.to change(Place, :count).by(2) place = Place.find_by(name: 'Test Location, Test Street') expect(place.city).to eq('Test City') expect(place.country).to eq('Test Country') expect(place.source).to eq('photon') end it 'returns both main place and suggested places' do result = subject.find_or_create_place(visit_data) expect(result[:main_place].name).to include('Test Location') expect(result[:suggested_places].length).to eq(2) expect(result[:suggested_places].map(&:name)).to include('Test Location, Test Street', 'Other Location, Other Street') end context 'when geocoding returns no results' do before do allow(Geocoder).to receive(:search).and_return([]) end it 'creates a place with the suggested name' do expect do result = subject.find_or_create_place(visit_data) expect(result[:main_place].name).to eq('Test Place') end.to change(Place, :count).by(1) place = Place.last expect(place.name).to eq('Test Place') expect(place.source).to eq('manual') end it 'returns the created place as both main and the only suggested place' do result = subject.find_or_create_place(visit_data) expect(result[:main_place].name).to eq('Test Place') expect(result[:suggested_places]).to eq([result[:main_place]]) end it 'falls back to default name when suggested name is missing' do visit_data_without_name = visit_data.merge(suggested_name: nil) result = subject.find_or_create_place(visit_data_without_name) expect(result[:main_place].name).to eq(Place::DEFAULT_NAME) end end end context 'with multiple potential places' do let!(:place1) { create(:place, name: 'Place 1', latitude: latitude, longitude: longitude) } let!(:place2) { create(:place, name: 'Place 2', latitude: latitude + 0.0005, longitude: longitude + 0.0005) } let!(:place3) { create(:place, name: 'Place 3', latitude: latitude + 0.001, longitude: longitude + 0.001) } it 'selects the closest place as main_place' do result = subject.find_or_create_place(visit_data) expect(result[:main_place]).to eq(place1) end it 'includes nearby places as suggested_places' do result = subject.find_or_create_place(visit_data) expect(result[:suggested_places]).to include(place1, place2) # place3 might be outside the search radius depending on the constants defined end it 'deduplicates places by name' do # Create a duplicate place with the same name create(:place, name: 'Place 1', latitude: latitude + 0.0002, longitude: longitude + 0.0002) result = subject.find_or_create_place(visit_data) names = result[:suggested_places].map(&:name) expect(names.count('Place 1')).to eq(1) end end context 'with API place creation failures' do let(:invalid_geocoder_result) do double( data: { 'properties' => { # Missing required fields } }, latitude: latitude, longitude: longitude ) end before do allow(Geocoder).to receive(:search).and_return([invalid_geocoder_result]) end it 'gracefully handles errors in place creation' do allow(subject).to receive(:create_place_from_api_result).and_call_original result = subject.find_or_create_place(visit_data) # Should create the default place expect(result[:main_place].name).to eq('Test Place') expect(result[:main_place].source).to eq('manual') end end end describe 'private methods' do context '#build_place_name' do it 'combines name components correctly' do properties = { 'name' => 'Coffee Shop', 'street' => 'Main St', 'housenumber' => '123', 'city' => 'New York' } name = subject.send(:build_place_name, properties) expect(name).to eq('Coffee Shop, Main St, 123, New York') end it 'removes duplicate components' do properties = { 'name' => 'Coffee Shop', 'street' => 'Coffee Shop', # Duplicate of name 'city' => 'New York' } name = subject.send(:build_place_name, properties) expect(name).to eq('Coffee Shop, New York') end it 'returns default name when no components are available' do properties = { 'other' => 'irrelevant' } name = subject.send(:build_place_name, properties) expect(name).to eq(Place::DEFAULT_NAME) end end end end