# frozen_string_literal: true

RSpec.describe HasAuxTable do
  LOG_QUERIES = false

  # Helper method to count queries
  def count_queries(&block)
    query_count = 0
    query_callback =
      lambda { |name, start, finish, message_id, values| query_count += 1 }

    ActiveSupport::Notifications.subscribed(
      query_callback,
      "sql.active_record"
    ) do
      if LOG_QUERIES
        old_logger = ActiveRecord::Base.logger
        ActiveRecord::Base.logger = Logger.new(STDOUT)
        ActiveRecord::Base.logger.level = Logger::DEBUG
      end
      block.call
      ActiveRecord::Base.logger = old_logger if LOG_QUERIES
    end

    query_count
  end

  before(:all) do
    # Set up the database schema for testing
    ActiveRecord::Schema.define do
      create_base_table :vehicles do |t|
        t.string :name
        t.timestamps

        t.create_aux :car do |t|
          t.string :fuel_type
          t.decimal :engine_size, precision: 3, scale: 1
        end

        t.create_aux :boat do |t|
          t.string :boat_type
        end
      end

      create_base_table :people do |t|
        t.string :name
        t.timestamps

        t.create_aux :driver do |t|
          t.references :car,
                       null: false,
                       foreign_key: {
                         to_table: :vehicles_car_aux,
                         primary_key: :base_table_id
                       }
        end

        t.create_aux :captain do |t|
          t.references :boat,
                       null: false,
                       foreign_key: {
                         to_table: :vehicles_boat_aux,
                         primary_key: :base_table_id
                       }
        end

        t.create_aux :passenger do |t|
          t.references :boat,
                       null: false,
                       foreign_key: {
                         to_table: :vehicles_boat_aux,
                         primary_key: :base_table_id
                       }
        end
      end
    end

    class Vehicle < ActiveRecord::Base
      include HasAuxTable
    end

    class Car < Vehicle
      aux_table :car
      has_many :drivers, inverse_of: :car
    end

    class Boat < Vehicle
      aux_table :boat
      has_many :passengers, inverse_of: :boat
      belongs_to :captain, inverse_of: :boat
    end

    class Person < ActiveRecord::Base
      include HasAuxTable
      self.table_name = "people"
    end

    class Driver < Person
      aux_table :driver
      belongs_to :car, inverse_of: :drivers
    end

    class Captain < Person
      aux_table :captain
      has_one :boat, inverse_of: :captain
    end

    class Passenger < Person
      aux_table :passenger
      belongs_to :boat, inverse_of: :passengers
    end
  end

  # Car class will be defined after schema setup

  it "has a version number" do
    expect(HasAuxTable::VERSION).not_to be nil
  end

  it "can create STI records" do
    car = Car.create!(name: "Toyota Camry", type: "Car")
    expect(car).to be_persisted

    boat = Boat.create!(name: "Yacht", type: "Boat")
    expect(boat).to be_persisted
  end

  it "is a clean test environment" do
    expect(Vehicle.count).to eq(0)
  end

  describe "column reporting" do
    it "reports the correct columns on the string repr of the class" do
      expect(Car.inspect).to include("fuel_type")
    end

    it "does not include the aux table foreign key" do
      expect(Car.inspect).not_to include("base_table_id")
    end
  end

  describe "database integration" do
    it "provides automatic attribute accessors for auxiliary table columns" do
      vehicle = Car.create!(name: "Honda Civic")

      # Test getter methods (should return nil initially)
      expect(vehicle.fuel_type).to be_nil
      expect(vehicle.engine_size).to be_nil

      # Test presence check methods
      expect(vehicle.fuel_type?).to be_falsey
      expect(vehicle.engine_size?).to be_falsey

      # Test setter methods (should create auxiliary record automatically)
      vehicle.fuel_type = "hybrid"
      vehicle.engine_size = 1.8

      # Test that values are set correctly
      expect(vehicle.fuel_type).to eq("hybrid")
      expect(vehicle.engine_size).to eq(1.8)

      # Test presence check methods after setting values
      expect(vehicle.fuel_type?).to be_truthy
      expect(vehicle.engine_size?).to be_truthy

      # Save and reload to verify persistence
      vehicle.save!
      reloaded_vehicle = Car.find(vehicle.id)
      expect(reloaded_vehicle.fuel_type).to eq("hybrid")
      expect(reloaded_vehicle.engine_size).to eq(1.8)
    end

    it "allows saving the model with auxiliary columns" do
      car = Car.create!(name: "Honda Civic")
      num_queries =
        count_queries do
          car.fuel_type = "hybrid"
          car.engine_size = 1.8
          car.save!
        end
      expect(num_queries).to eq(1)
    end
  end

  describe "query extensions" do
    before do
      # Create test data
      @car1 =
        Car.create!(
          name: "Toyota Prius",
          type: "Car",
          fuel_type: "hybrid",
          engine_size: 1.8
        )

      @car2 =
        Car.create!(
          name: "Honda Civic",
          type: "Car",
          fuel_type: "gasoline",
          engine_size: 2.0
        )

      @car3 =
        Car.create!(
          name: "Tesla Model 3",
          type: "Car",
          fuel_type: "electric",
          engine_size: 0.0
        )
    end

    describe "find method with automatic joins" do
      it "automatically includes auxiliary table joins for find" do
        # Find should automatically include joins to load auxiliary data
        found_car = Car.find(@car1.id)

        # Auxiliary attributes should be accessible
        expect(found_car.fuel_type).to eq("hybrid")
        expect(found_car.engine_size).to eq(1.8)
        expect(found_car.name).to eq("Toyota Prius")
      end

      it "works with multiple IDs" do
        cars = Car.find([@car1.id, @car2.id])
        expect(cars.length).to eq(2)

        # All cars should have auxiliary data loaded
        prius = cars.find { |c| c.name == "Toyota Prius" }
        civic = cars.find { |c| c.name == "Honda Civic" }

        expect(prius.fuel_type).to eq("hybrid")
        expect(civic.fuel_type).to eq("gasoline")
      end
    end

    describe "find_by method with automatic joins" do
      it "automatically includes auxiliary table joins for find_by" do
        # Find_by should automatically include joins for auxiliary data
        found_car = Car.find_by(name: "Toyota Prius")

        expect(found_car).to be_present
        expect(found_car.fuel_type).to eq("hybrid")
        expect(found_car.engine_size).to eq(1.8)
      end

      it "works with auxiliary columns in find_by" do
        # This should work with auxiliary columns due to automatic join
        found_car = Car.find_by(fuel_type: "hybrid")

        expect(found_car).to be_present
        expect(found_car.name).to eq("Toyota Prius")
        expect(found_car.fuel_type).to eq("hybrid")
      end

      it "returns nil when no record found" do
        found_car = Car.find_by(fuel_type: "diesel")
        expect(found_car).to be_nil
      end
    end

    describe "where method with automatic joins" do
      it "automatically handles auxiliary columns in where clauses" do
        # Query with auxiliary column should automatically include join
        hybrid_cars = Car.where(fuel_type: "hybrid")
        expect(hybrid_cars.length).to eq(1)
        expect(hybrid_cars.first.name).to eq("Toyota Prius")
        expect(hybrid_cars.first.fuel_type).to eq("hybrid")
      end

      it "works with multiple auxiliary columns" do
        # Query with multiple auxiliary columns
        efficient_cars = Car.where(fuel_type: "hybrid", engine_size: 1.8)

        expect(efficient_cars.length).to eq(1)
        expect(efficient_cars.first.name).to eq("Toyota Prius")
      end

      it "handles range queries on auxiliary columns" do
        # Range query on auxiliary column
        small_engine_cars = Car.where(engine_size: 0.0..1.9)

        expect(small_engine_cars.length).to eq(2)
        car_names = small_engine_cars.map(&:name).sort
        expect(car_names).to eq(["Tesla Model 3", "Toyota Prius"])
      end

      it "supports mixed queries with main table and auxiliary table columns" do
        # Mixed query with both main table and auxiliary table columns
        prius_hybrids = Car.where(name: "Toyota Prius", fuel_type: "hybrid")

        expect(prius_hybrids.length).to eq(1)
        expect(prius_hybrids.first.name).to eq("Toyota Prius")
        expect(prius_hybrids.first.fuel_type).to eq("hybrid")
      end

      it "handles IN queries on auxiliary columns" do
        # IN query on auxiliary column
        eco_cars = Car.where(fuel_type: %w[hybrid electric])

        expect(eco_cars.length).to eq(2)
        car_names = eco_cars.map(&:name).sort
        expect(car_names).to eq(["Tesla Model 3", "Toyota Prius"])
      end

      it "doesn't add joins for queries without auxiliary columns" do
        toyota_cars = Car.where(name: "Toyota Prius")
        expect(toyota_cars.length).to eq(1)
        expect(toyota_cars.first.name).to eq("Toyota Prius")
        expect(toyota_cars.first.fuel_type).to eq("hybrid")
      end

      it "works with chained where clauses" do
        # Chain where clauses with auxiliary columns

        efficient_cars = Car.where(fuel_type: "hybrid").where(engine_size: 1.8)

        expect(efficient_cars.length).to eq(1)
        expect(efficient_cars.first.name).to eq("Toyota Prius")
      end

      it "supports complex query combinations" do
        # Complex query with OR conditions
        cars = Car.where(fuel_type: "hybrid").or(Car.where(engine_size: 0.0))

        expect(cars.length).to eq(2)
        car_names = cars.map(&:name).sort
        expect(car_names).to eq(["Tesla Model 3", "Toyota Prius"])
      end
    end

    describe "query performance and optimization" do
      it "loads auxiliary data in single query with joins" do
        # This test ensures we're using joins rather than N+1 queries
        cars = Car.where(fuel_type: "gasoline")

        # Should have loaded auxiliary data via join
        expect(cars.length).to eq(1)
        expect(cars.first.name).to eq("Honda Civic")
        expect(cars.first.fuel_type).to eq("gasoline")
        expect(cars.first.engine_size).to eq(2.0)
      end

      describe "query count validation" do
        # These tests validate the performance optimizations using eager_load
        # All query methods now use single queries with proper LEFT OUTER JOINs

        it "loads single model with auxiliary data in one query using find" do
          query_count =
            count_queries do
              car = Car.find(@car1.id)
              # Access auxiliary attributes to ensure they're loaded
              car.fuel_type
              car.engine_size
            end

          expect(query_count).to eq(1)
        end

        it "loads single model with auxiliary data in one query using find_by" do
          query_count =
            count_queries do
              car = Car.find_by(name: "Toyota Prius")
              # Access auxiliary attributes to ensure they're loaded
              car.fuel_type
              car.engine_size
            end

          expect(query_count).to eq(1)
        end

        it "loads multiple models with auxiliary data in one query using where" do
          query_count =
            count_queries do
              cars = Car.where(fuel_type: %w[hybrid electric])
              # Access auxiliary attributes for all cars
              cars.each do |car|
                car.fuel_type
                car.engine_size
              end
            end

          expect(query_count).to eq(1)
        end

        it "avoids N+1 queries when loading multiple models" do
          # Create additional test data

          5.times do |i|
            Car.create!(
              name: "Test Car #{i}",
              fuel_type: "gasoline",
              engine_size: 1.5
            )
          end

          cars = nil
          query_count =
            count_queries do
              cars = Car.where(fuel_type: "gasoline")
              # Access auxiliary attributes for all cars - should not trigger additional queries
              cars.each do |car|
                car.fuel_type
                car.engine_size
                car.name
              end
            end

          expect(query_count).to eq(1) # Single query regardless of how many cars are loaded
          expect(cars.length).to be >= 1 # At least the original Honda Civic plus new cars
        end

        it "uses single query when ordering by auxiliary columns" do
          query_count =
            count_queries do
              cars = Car.where(engine_size: 1.0..3.0).order(:engine_size)
              # Access all attributes
              cars.each do |car|
                car.name
                car.fuel_type
                car.engine_size
              end
            end

          expect(query_count).to eq(1)
        end

        it "uses single query for complex auxiliary column queries" do
          query_count =
            count_queries do
              cars =
                Car.where(fuel_type: "hybrid").or(Car.where(engine_size: 0.0))
              # Access all attributes
              cars.each do |car|
                car.name
                car.fuel_type
                car.engine_size
              end
            end

          expect(query_count).to eq(1)
        end

        it "uses single query when finding by auxiliary columns" do
          query_count =
            count_queries do
              car = Car.find_by(fuel_type: "hybrid", name: "Toyota Prius")
              # Access all attributes
              car.name
              car.fuel_type
              car.engine_size
            end

          expect(query_count).to eq(1)
        end

        it "doesn't trigger additional queries when accessing auxiliary attributes after load" do
          # First load the car
          car = Car.find(@car1.id)

          # Now count queries when accessing auxiliary attributes
          query_count =
            count_queries do
              car.fuel_type
              car.engine_size
              car.fuel_type? # presence check
              car.engine_size? # presence check
            end

          # Currently this should be 0 since auxiliary record is already loaded
          expect(query_count).to eq(0) # No additional queries should be triggered
        end

        it "handles mixed queries with main and auxiliary columns in single query" do
          query_count =
            count_queries do
              cars = Car.where(name: "Toyota Prius", fuel_type: "hybrid")
              cars.each do |car|
                car.name
                car.fuel_type
                car.engine_size
              end
            end

          expect(query_count).to eq(1)
        end

        it "uses single query for range queries on auxiliary columns" do
          query_count =
            count_queries do
              cars = Car.where(engine_size: 0.0..1.9)
              cars.each do |car|
                car.name
                car.fuel_type
                car.engine_size
              end
            end

          expect(query_count).to eq(1)
        end

        it "maintains single query performance with limit and offset" do
          query_count =
            count_queries do
              car = Car.where(fuel_type: %w[hybrid electric]).limit(1).first
              # Access auxiliary attributes
              car.fuel_type
              car.engine_size
            end

          expect(query_count).to eq(1)
        end
      end

      it "works with order clauses on auxiliary columns" do
        # Order by auxiliary column
        cars_by_engine = Car.where(engine_size: 1.0..3.0).order(:engine_size)

        expect(cars_by_engine.length).to eq(2)
        expect(cars_by_engine.first.name).to eq("Toyota Prius")
        expect(cars_by_engine.second.name).to eq("Honda Civic")
      end

      it "supports limit and offset with auxiliary columns" do
        # Limit with auxiliary column query
        first_hybrid = Car.where(fuel_type: %w[hybrid electric]).limit(1).first

        expect(first_hybrid).to be_present
        expect(["Toyota Prius", "Tesla Model 3"]).to include(first_hybrid.name)
      end
    end

    describe "edge cases and error handling" do
      it "handles queries with non-existent auxiliary columns gracefully" do
        # This should not break and should fall back to normal query behavior
        expect { Car.where(non_existent_column: "value").first }.to raise_error(
          ActiveRecord::StatementInvalid
        )
      end

      it "works with empty where conditions" do
        # Empty where should not cause issues
        cars = Car.where({})
        expect(cars.length).to eq(3)
      end

      it "handles nil values in auxiliary columns" do
        # Create a car with nil auxiliary values
        Car.create!(name: "Incomplete Car", type: "Car")
        Car.create!(
          name: "Complete Car",
          fuel_type: "gasoline",
          engine_size: 2.0
        )

        # Query for cars with nil fuel_type
        incomplete_cars = Car.where(fuel_type: nil)
        expect(incomplete_cars.length).to eq(1)
        expect(incomplete_cars.first.name).to eq("Incomplete Car")
      end
    end
  end

  describe "column overlap validation" do
    it "raises error when auxiliary table defines column that exists in main table" do
      # Create a test schema with overlapping columns
      ActiveRecord::Schema.define do
        create_base_table :test_overlap_mains do |t|
          t.string :name
          t.string :description

          t.create_aux :overlap do |t|
            t.string :name
            t.string :extra_data
          end

          t.timestamps
        end
      end

      # Define models that will trigger the validation
      class TestOverlapMain < ActiveRecord::Base
        include HasAuxTable
      end

      expect {
        class TestOverlapChild < TestOverlapMain
          aux_table :overlap
        end

        # Trigger schema loading to activate validation
        TestOverlapChild.load_schema
      }.to raise_error(ArgumentError, /defines column\(s\) 'name'/)
    end

    it "ignores system columns and foreign keys when checking for overlaps" do
      # Create a test schema where system columns are duplicated (which should be allowed)
      ActiveRecord::Schema.define do
        create_base_table :test_system_cols_main do |t|
          t.string :name

          t.create_aux :has_timestamps do |t|
            t.timestamps
          end

          t.timestamps
        end
      end

      class TestSystemColsMain < ActiveRecord::Base
        include HasAuxTable
      end

      expect {
        class TestSystemColsChild < TestSystemColsMain
          aux_table :has_timestamps
        end
      }.not_to raise_error
    end
  end

  describe "methods that depend on relation" do
    before(:each) do
      @car =
        Car.create!(name: "Toyota Prius", fuel_type: "hybrid", engine_size: 1.5)
    end

    describe "destroy" do
      it "destroys the main record" do
        expect { @car.destroy }.to change { Car.count }.by(-1)
      end

      it "destroys the aux record" do
        expect { @car.destroy }.to change {
          Object.const_get(:VehiclesCarAux).count
        }.by(-1)
      end
    end

    describe "nested associations" do
      it "can create a driver through the association" do
        driver = @car.drivers.create!(name: "John Doe")
        expect(driver.car).to eq(@car)
        expect(driver.car_id).to eq(@car.id)
        expect(driver.car.fuel_type).to eq("hybrid")
        expect(driver.car.engine_size).to eq(1.5)
      end

      it "can create a driver directly" do
        driver = Driver.create!(car: @car, name: "John Doe")
        expect(driver.car).to eq(@car)
        expect(driver.car_id).to eq(@car.id)
        expect(driver.car.fuel_type).to eq("hybrid")
        expect(driver.car.engine_size).to eq(1.5)
      end

      it "can be accessed through the association" do
        driver = @car.drivers.create!(name: "John Doe")
        expect(@car.drivers).to eq([driver])
      end

      it "can be destroyed through the association" do
        driver = @car.drivers.create!(name: "John Doe")
        expect { driver.destroy }.to change { @car.reload.drivers.count }.by(-1)
      end
    end

    describe "#reload" do
      it "discards changes to aux attributes when reloading the model" do
        @car.fuel_type = "gasoline"
        @car.reload
        expect(@car.fuel_type).to eq("hybrid")
      end

      it "discards changes to main attributes when reloading the model" do
        @car.name = "Honda Civic"
        @car.reload
        expect(@car.name).to eq("Toyota Prius")
      end

      it "can be saved after reloading" do
        @car.reload
        @car.name = "Honda Civic"
        @car.save!

        car = Car.find(@car.id)
        expect(car.name).to eq("Honda Civic")
      end

      it "reloads with one query" do
        num_queries = count_queries { @car.reload }
        expect(num_queries).to eq(1)
      end
    end

    describe "#exists?" do
      it "works when present with base table attributes" do
        expect(Car.exists?(id: @car.id)).to be_truthy
      end

      it "works when missing with with base table attributes" do
        expect(Car.exists?(id: 9999)).to be_falsey
      end

      it "works when present with aux table attributes" do
        expect(Car.exists?(fuel_type: "hybrid")).to be_truthy
      end

      it "works when missing with aux table attributes" do
        expect(Car.exists?(fuel_type: "diesel")).to be_falsey
      end
    end
  end
end
