# frozen_string_literal: true

require 'spec_helper'

RSpec.describe "Database" do
  let(:models_todo) {
    ["Spree::CreditCard", "Spree::Adjustment",
     "StripeAccount", "ColumnPreference",
     "Spree::LineItem", "Spree::ShippingMethod",
     "Spree::ShippingRate"].freeze
  }

  it "should have foreign keys for models with a belongs_to relationship" do
    Rails.application.eager_load!
    model_classes = filter_model_classes

    migrations = generate_migrations(model_classes)

    expect(migrations.length).to eq(0)
  end

  def filter_model_classes
    Dir.glob(Rails.root.join('app/models/**/*.rb').to_s)
      .map do |file|
        relative_path = Pathname.new(file).relative_path_from(Rails.root.join('app/models')).to_s
        subdirectory = File.dirname(relative_path)
        base_name = File.basename(file, '.rb').camelize
        subdirectory == "." ? base_name : "#{subdirectory.camelize}::#{base_name}"
      end
  end

  def generate_migrations(model_classes)
    migrations = []
    filter = lambda { |model| models_todo.include?(model) }
    pending_models = model_classes.select(&filter)
    model_classes.reject!(&filter)

    ActiveRecord::Base.descendants.each do |model_class|
      next unless model_classes.include?(model_class.name)

      model_class.reflect_on_all_associations(:belongs_to).each do |association|
        migration = process_association(model_class, association)
        migrations << migration unless migration.nil?
      end
    end

    print_missing_foreign_key_warnings(migrations)

    puts "The following models are marked as todo in #{__FILE__}:"
    puts pending_models.join(", ")

    migrations
  end

  def print_missing_foreign_key_warnings(migrations)
    return if migrations.empty?

    puts "Foreign key(s) appear to be absent from the database. " \
         "You can add it/them using the following migration(s):"
    puts migrations.join("\n")
    puts "\nTo disable this warning, add the class name(s) of the model(s) to models_todo " \
         "in #{__FILE__}"
  end

  def process_association(model_class, association)
    return if association.options[:polymorphic] || association.options[:optional]

    foreign_key_table_name = determine_foreign_key_table_name(model_class, association)
    foreign_key_column = association.options[:foreign_key] || "#{association.name}_id"
    foreign_keys = model_class.connection.foreign_keys(model_class.table_name)

    # Check if there is a foreign key that already exists for the column
    return if foreign_keys.any? { |fk|
                fk.column == foreign_key_column &&
                fk.to_table == foreign_key_table_name
              }

    generate_migration(model_class, foreign_key_table_name, foreign_key_column)
  end

  def determine_foreign_key_table_name(model_class, association)
    if association.options[:class_name]
      class_name = association.options[:class_name].underscore.parameterize
      foreign_key_table_name = class_name.tableize
    else
      foreign_key_table_name = association.class_name.underscore.parameterize.tableize
      namespace = model_class.name.deconstantize

      unless association.class_name.deconstantize == namespace || namespace == "" ||
             ActiveRecord::Base.connection.table_exists?(foreign_key_table_name)
        foreign_key_table_name = "#{namespace.underscore}_#{foreign_key_table_name}"
      end
    end

    foreign_key_table_name
  end

  def generate_migration(model_class, foreign_key_table_name, foreign_key_column)
    migration_name = "add_foreign_key_to_#{model_class.table_name}_" \
                     "#{foreign_key_table_name}_#{foreign_key_column}"
    migration_class_name = migration_name.camelize
    orphaned_records_query = generate_orphaned_records_query(model_class, foreign_key_table_name,
                                                             foreign_key_column)

    <<~MIGRATION
      # Orphaned records can be found before running this migration with the following SQL:

      #{orphaned_records_query}

      class #{migration_class_name} < ActiveRecord::Migration[6.0]
        def change
          add_foreign_key :#{model_class.table_name}, :#{foreign_key_table_name}, column: :#{foreign_key_column}
        end
      end
    MIGRATION
  end

  def generate_orphaned_records_query(model_class, foreign_key_table_name, foreign_key_column)
    <<~SQL # rubocop:disable Rails/SquishedSQLHeredocs # Using squish deletes the newlines
      # SELECT COUNT(*)
      # FROM #{model_class.table_name}
      # LEFT JOIN #{foreign_key_table_name}
      #   ON #{model_class.table_name}.#{foreign_key_column} = #{foreign_key_table_name}.id
      # WHERE #{foreign_key_table_name}.id IS NULL
      #   AND #{model_class.table_name}.#{foreign_key_column} IS NOT NULL
    SQL
  end
end
