# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Visits::Creator do
  let(:user) { create(:user) }

  subject { described_class.new(user) }

  describe '#create_visits' do
    let(:point1) { create(:point, user: user) }
    let(:point2) { create(:point, user: user) }

    let(:visit_data) do
      {
        start_time: 1.hour.ago.to_i,
        end_time: 30.minutes.ago.to_i,
        duration: 30.minutes.to_i,
        center_lat: 40.7128,
        center_lon: -74.0060,
        radius: 50,
        suggested_name: 'Test Place',
        points: [point1, point2]
      }
    end

    context 'when a confirmed visit already exists at the same location' do
      let(:place) { create(:place, lonlat: 'POINT(-74.0060 40.7128)', name: 'Existing Place') }
      let!(:existing_visit) do
        create(
          :visit,
          user: user,
          place: place,
          status: :confirmed,
          started_at: 1.5.hours.ago,
          ended_at: 45.minutes.ago,
          duration: 45
        )
      end

      it 'returns the existing confirmed visit instead of creating a duplicate suggested visit' do
        visits = subject.create_visits([visit_data])

        expect(visits.size).to eq(1)
        expect(visits.first).to eq(existing_visit)
        expect(visits.first.status).to eq('confirmed')

        # Verify no new visits were created
        expect(Visit.count).to eq(1)
      end

      it 'does not change points associations' do
        original_visit_id = point1.visit_id

        subject.create_visits([visit_data])

        # Points should remain unassociated
        expect(point1.reload.visit_id).to eq(original_visit_id)
        expect(point2.reload.visit_id).to eq(nil)
      end
    end

    context 'when a confirmed visit exists but at a different location' do
      let(:different_place) { create(:place, lonlat: 'POINT(-73.9000 41.0000)', name: 'Different Place') }
      let!(:existing_visit) do
        create(
          :visit,
          user: user,
          place: different_place,
          status: :confirmed,
          started_at: 1.5.hours.ago,
          ended_at: 45.minutes.ago,
          duration: 45
        )
      end
      let(:place) { create(:place, lonlat: 'POINT(-74.0060 40.7128)', name: 'New Place') }
      let(:place_finder) { instance_double(Visits::PlaceFinder) }

      before do
        allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
        allow(place_finder).to receive(:find_or_create_place).and_return({ main_place: place, suggested_places: [] })
      end

      it 'creates a new suggested visit' do
        visits = subject.create_visits([visit_data])

        expect(visits.size).to eq(1)
        expect(visits.first).not_to eq(existing_visit)
        expect(visits.first.place).to eq(place)
        expect(visits.first.status).to eq('suggested')

        # Should now have two visits
        expect(Visit.count).to eq(2)
      end
    end

    context 'when matching an area' do
      let!(:area) { create(:area, user: user, latitude: 40.7128, longitude: -74.0060, radius: 100) }

      it 'creates a visit associated with the area' do
        visits = subject.create_visits([visit_data])

        expect(visits.size).to eq(1)
        visit = visits.first

        expect(visit.area).to eq(area)
        expect(visit.place).to be_nil
        expect(visit.started_at).to be_within(1.second).of(Time.zone.at(visit_data[:start_time]))
        expect(visit.ended_at).to be_within(1.second).of(Time.zone.at(visit_data[:end_time]))
        expect(visit.duration).to eq(30)
        expect(visit.name).to eq(area.name)
        expect(visit.status).to eq('suggested')

        expect(point1.reload.visit_id).to eq(visit.id)
        expect(point2.reload.visit_id).to eq(visit.id)
      end

      it 'uses area name for visit name' do
        area.update(name: 'Custom Area Name')
        visits = subject.create_visits([visit_data])

        expect(visits.first.name).to eq('Custom Area Name')
      end

      it 'does not find areas too far from the visit center' do
        far_area = create(:area, user: user, latitude: 41.8781, longitude: -87.6298, radius: 100) # Chicago

        visits = subject.create_visits([visit_data])

        expect(visits.first.area).to eq(area) # Should match the closer area
        expect(visits.first.area).not_to eq(far_area)
      end
    end

    context 'when matching a place' do
      let(:place) { create(:place, name: 'Test Place') }
      let(:suggested_place1) { create(:place, name: 'Suggested Place 1') }
      let(:suggested_place2) { create(:place, name: 'Suggested Place 2') }
      let(:place_finder) { instance_double(Visits::PlaceFinder) }
      let(:place_data) do
        {
          main_place: place,
          suggested_places: [suggested_place1, suggested_place2]
        }
      end

      before do
        allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
        allow(place_finder).to receive(:find_or_create_place).and_return(place_data)
      end

      it 'creates a visit associated with the place' do
        visits = subject.create_visits([visit_data])

        expect(visits.size).to eq(1)
        visit = visits.first

        expect(visit.area).to be_nil
        expect(visit.place).to eq(place)
        expect(visit.name).to eq(place.name)
      end

      it 'associates suggested places with the visit' do
        visits = subject.create_visits([visit_data])
        visit = visits.first

        # Check for place_visits associations
        expect(visit.place_visits.count).to eq(2)
        expect(visit.place_visits.pluck(:place_id)).to contain_exactly(
          suggested_place1.id,
          suggested_place2.id
        )
        expect(visit.suggested_places).to contain_exactly(suggested_place1, suggested_place2)
      end

      it 'does not create duplicate place_visit associations' do
        # Create an existing association
        visit = create(:visit, user: user, place: place)
        create(:place_visit, visit: visit, place: suggested_place1)

        allow(Visit).to receive(:create!).and_return(visit)

        # Only one new association should be created
        expect do
          subject.create_visits([visit_data])
        end.to change(PlaceVisit, :count).by(1)
        expect(visit.place_visits.pluck(:place_id)).to contain_exactly(
          suggested_place1.id,
          suggested_place2.id
        )
      end
    end

    context 'when no area or place is found' do
      let(:place_finder) { instance_double(Visits::PlaceFinder) }

      before do
        allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
        allow(place_finder).to receive(:find_or_create_place).and_return(nil)
      end

      it 'uses suggested name from visit data' do
        visits = subject.create_visits([visit_data])

        expect(visits.first.area).to be_nil
        expect(visits.first.place).to be_nil
        expect(visits.first.name).to eq('Test Place')
      end

      it 'uses "Unknown Location" when no name is available' do
        visit_data_without_name = visit_data.dup
        visit_data_without_name[:suggested_name] = nil

        visits = subject.create_visits([visit_data_without_name])

        expect(visits.first.name).to eq('Unknown Location')
      end
    end

    context 'when processing multiple visits' do
      let(:place1) { create(:place, name: 'Place 1') }
      let(:place2) { create(:place, name: 'Place 2') }
      let(:place_finder) { instance_double(Visits::PlaceFinder) }

      let(:visit_data2) do
        {
          start_time: 3.hours.ago.to_i,
          end_time: 2.hours.ago.to_i,
          duration: 60.minutes.to_i,
          center_lat: 41.8781,
          center_lon: -87.6298,
          radius: 50,
          suggested_name: 'Chicago Visit',
          points: [create(:point, user: user), create(:point, user: user)]
        }
      end

      before do
        allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
        allow(place_finder).to receive(:find_or_create_place)
          .with(visit_data).and_return({ main_place: place1, suggested_places: [] })
        allow(place_finder).to receive(:find_or_create_place)
          .with(visit_data2).and_return({ main_place: place2, suggested_places: [] })
      end

      it 'creates multiple visits' do
        visits = subject.create_visits([visit_data, visit_data2])

        expect(visits.size).to eq(2)
        expect(visits[0].place).to eq(place1)
        expect(visits[0].name).to eq('Place 1')
        expect(visits[1].place).to eq(place2)
        expect(visits[1].name).to eq('Place 2')
      end
    end

    context 'when transaction fails' do
      let(:place_finder) { instance_double(Visits::PlaceFinder) }

      before do
        allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
        allow(place_finder).to receive(:find_or_create_place).and_return(nil)
        allow(Visit).to receive(:create!).and_raise(ActiveRecord::RecordInvalid)
      end

      it 'does not update points if visit creation fails' do
        expect do
          subject.create_visits([visit_data])
        end.to raise_error(ActiveRecord::RecordInvalid)

        # Points should not be associated with any visit
        expect(point1.reload.visit_id).to be_nil
        expect(point2.reload.visit_id).to be_nil
      end
    end
  end

  describe '#find_matching_area' do
    let(:visit_data) do
      {
        center_lat: 40.7128,
        center_lon: -74.0060,
        radius: 50
      }
    end

    it 'finds areas within radius' do
      area_within = create(:area, user: user, latitude: 40.7129, longitude: -74.0061, radius: 100)
      area_outside = create(:area, user: user, latitude: 40.7500, longitude: -74.0500, radius: 100)

      result = subject.send(:find_matching_area, visit_data)
      expect(result).to eq(area_within)
    end

    it 'returns nil when no areas match' do
      create(:area, user: user, latitude: 42.0, longitude: -72.0, radius: 100)

      result = subject.send(:find_matching_area, visit_data)
      expect(result).to be_nil
    end

    it 'only considers user areas' do
      area_other_user = create(:area, latitude: 40.7128, longitude: -74.0060, radius: 100)
      area_user = create(:area, user: user, latitude: 40.7128, longitude: -74.0060, radius: 100)

      result = subject.send(:find_matching_area, visit_data)
      expect(result).to eq(area_user)
    end
  end

  describe '#near_area?' do
    it 'returns true when point is within area radius' do
      area = create(:area, latitude: 40.7128, longitude: -74.0060, radius: 100)
      center = [40.7129, -74.0061] # Very close to area center

      result = subject.send(:near_area?, center, area)
      expect(result).to be true
    end

    it 'returns false when point is outside area radius' do
      area = create(:area, latitude: 40.7128, longitude: -74.0060, radius: 100)
      center = [40.7500, -74.0500] # Further away

      result = subject.send(:near_area?, center, area)
      expect(result).to be false
    end
  end
end
