require 'spec_helper'

RSpec.describe 'TableInheritance' do
  let(:connection) { ActiveRecord::Base.connection }

  context 'on migration' do
    mock_create_table

    it 'does not affect some basic forms of table creation' do
      sql = connection.create_table('schema_migrations', id: false) do |t|
        t.string :version, **connection.internal_string_options_for_primary_key
      end

      result = 'CREATE TABLE "schema_migrations"'
      result << ' \("version" character varying( NOT NULL)? PRIMARY KEY\)'
      expect(sql).to match(/#{result}/)
    end

    it 'does not affect simple table creation' do
      sql = connection.create_table(:activities) do |t|
        t.string :title
        t.boolean :active
        t.timestamps
      end

      result = 'CREATE TABLE "activities" ('
      result << '"id" bigserial primary key'
      result << ', "title" character varying'
      result << ', "active" boolean'
      result << ', "created_at" timestamp(6) NOT NULL'
      result << ', "updated_at" timestamp(6) NOT NULL'
      result << ')'
      expect(sql).to eql(result)
    end

    it 'does not affect temporary table creation based on a query' do
      query = 'SELECT * FROM "authors"'
      sql = connection.create_table(:test, temporary: true, as: query)

      result = 'CREATE TEMPORARY TABLE "test"'
      result << "  AS #{query}"
      expect(sql).to eql(result)
    end

    it 'adds the inherits statement for a single inheritance' do
      sql = connection.create_table(:activity_videos, inherits: :activities) do |t|
        t.string :url
      end

      result = 'CREATE TABLE "activity_videos" ('
      result << '"url" character varying'
      result << ') INHERITS ( "activities" )'
      expect(sql).to eql(result)
    end

    it 'adds the inherits statement for a multiple inheritance' do
      sql = connection.create_table(:activity_tests, inherits: [:activities, :tests]) do |t|
        t.string :grade
      end

      result = 'CREATE TABLE "activity_tests" ('
      result << '"grade" character varying'
      result << ') INHERITS ( "activities" , "tests" )'
      expect(sql).to eql(result)
    end

    it 'allows empty-body create table operation' do
      sql = connection.create_table(:activity_posts, inherits: :activities)
      result = "CREATE TABLE \"activity_posts\" ()"
      result << ' INHERITS ( "activities" )'
      expect(sql).to eql(result)
    end
  end

  context 'on schema' do
    let(:source) { ActiveRecord::Base.connection_pool }
    let(:dump_result) do
      ActiveRecord::SchemaDumper.dump(source, (dump_result = StringIO.new))
      dump_result.string
    end

    it 'dumps single inheritance with body' do
      parts = '"activity_books"'
      parts << ', id: false'
      parts << ', inherits: "activities"'
      parts << ', force: :cascade'
      expect(dump_result).to match(/create_table #{parts} do /)
    end

    it 'dumps single inheritance without body' do
      parts = '"activity_post_samples"'
      parts << ', id: false'
      parts << ', inherits: "activity_posts"'
      parts << ', force: :cascade'
      expect(dump_result).to match(/create_table #{parts}(?! do \|t\|)/)
    end

    it 'dumps multiple inheritance' do
      parts = '"activity_posts"'
      parts << ', id: false'
      parts << ', inherits: (\["images", "activities"\]|\["activities", "images"\])'
      parts << ', force: :cascade'
      expect(dump_result).to match(/create_table #{parts}/)
    end
  end

  context 'on schema cache' do
    let(:schema_cache) { ActiveRecord::Base.connection.schema_cache }
    let(:schema_cache_reflection) { schema_cache.instance_variable_get(:@schema_reflection) }
    let(:new_schema_cache) { schema_cache_reflection.send(:cache, schema_cache_source) }
    let(:schema_cache_source) { schema_cache.instance_variable_get(:@pool) }

    subject { new_schema_cache }

    it 'correctly defines the associations' do
      scenario = {
        'M' => %w(N),
        'N' => %w(C),
        'C' => %w(B),
        'B' => %w(A),
        'D' => %w(A),
        'F' => %w(E),
        'G' => %w(E H),
      }

      subject.instance_variable_set(:@inheritance_loaded, true)
      subject.instance_variable_set(:@inheritance_dependencies, scenario)
      subject.instance_variable_set(:@inheritance_associations, subject.send(:generate_associations))
      subject.instance_variable_set(:@data_sources_model_names, {})
      expect(subject.instance_variable_get(:@inheritance_associations)).to eql({
        'A' => %w(B D C N M),
        'B' => %w(C N M),
        'C' => %w(N M),
        'N' => %w(M),
        'E' => %w(F G),
        'H' => %w(G),
      })
    end

    context 'on looking up models' do
      let(:prepare_arguments) { [schema_cache_source] }
      let(:prepare_method) { :add_all }

      after(:all) do
        schema_cache = ActiveRecord::Base.connection.schema_cache
        schema_cache.instance_variable_set(:@data_sources, {})
        schema_cache.instance_variable_set(:@data_sources_model_names, {})
      end

      it 'respect irregular names' do
        allow(Torque::PostgreSQL.config).to receive(:irregular_models).and_return({
          'public.posts' => 'ActivityPost',
        })

        subject.send(prepare_method, *prepare_arguments)
        list = subject.instance_variable_get(:@data_sources_model_names)
        expect(list).to have_key('public.posts')
        expect(list['public.posts']).to eql(ActivityPost)
      end

      it 'does not load irregular where the data source is not defined' do
        allow(Torque::PostgreSQL.config).to receive(:irregular_models).and_return({
          'products' => 'Product',
        })

        subject.send(prepare_method, *prepare_arguments)
        list = subject.instance_variable_get(:@data_sources_model_names)
        expect(list).to_not have_key('products')
      end

      it 'works with eager loading' do
        allow(Torque::PostgreSQL.config).to receive(:eager_load).and_return(true)
        ActivityPost.reset_table_name

        list = subject.instance_variable_get(:@data_sources_model_names)
        expect(list).to have_key('activity_posts')
        expect(list['activity_posts']).to eql(ActivityPost)
      end

      {
        'activities' => 'Activity',
        'activity_posts' => 'ActivityPost',
        'activity_post_samples' => 'ActivityPost::Sample',
      }.each do |table_name, expected_model|
        it "translate the table name #{table_name} to #{expected_model} model" do
          expect(subject.lookup_model(table_name)).to eql(expected_model.constantize)
        end
      end
    end
  end

  context 'on inheritance' do
    let(:base) { Activity }
    let(:child) { ActivityPost }
    let(:child2) { ActivityBook }
    let(:other) { AuthorJournalist }

    before { ActiveRecord::Base.connection.schema_cache.clear! }

    it 'identifies mergeable attributes' do
      result_base = %w(id author_id title active kind created_at updated_at description url file post_id)
      expect(base.inheritance_mergeable_attributes.sort).to eql(result_base.sort)
    end

    it 'has a merged version of attributes' do
      result_base = %w(id author_id title active kind created_at updated_at description url activated file post_id)
      result_child = %w(id author_id title active kind created_at updated_at file post_id url activated)
      result_child2 = %w(id author_id title active kind created_at updated_at description url activated)
      result_other = %w(id name type specialty)

      expect(base.inheritance_merged_attributes).to eql(result_base)
      expect(child.inheritance_merged_attributes).to eql(result_child)
      expect(child2.inheritance_merged_attributes).to eql(result_child2)
      expect(other.inheritance_merged_attributes).to eql(result_other)
    end

    it 'identifies physical inheritance' do
      expect(base.physically_inherited?).to be_falsey
      expect(child.physically_inherited?).to be_truthy
      expect(child2.physically_inherited?).to be_truthy
      expect(other.physically_inherited?).to be_falsey
    end

    it 'returns a list of dependent tables' do
      expect(base.inheritance_dependents).to eql(%w(activity_books activity_posts activity_post_samples))
      expect(child.inheritance_dependents).to eql(%w(activity_post_samples))
      expect(child2.inheritance_dependents).to eql(%w())
      expect(other.inheritance_dependents).to eql(%w())
    end

    it 'can check dependency' do
      expect(base.physically_inheritances?).to be_truthy
      expect(child.physically_inheritances?).to be_truthy
      expect(child2.physically_inheritances?).to be_falsey
      expect(other.physically_inheritances?).to be_falsey
    end

    it 'returns the list of models that the records can be casted to' do
      expect(base.casted_dependents.values.map(&:name)).to eql(%w(ActivityBook ActivityPost ActivityPost::Sample))
      expect(child.casted_dependents.values.map(&:name)).to eql(%w(ActivityPost::Sample))
      expect(child2.casted_dependents.values.map(&:name)).to eql(%w())
      expect(other.casted_dependents.values.map(&:name)).to eql(%w())
    end

    it 'correctly generates the tables name' do
      expect(base.table_name).to eql('activities')
      expect(child.table_name).to eql('activity_posts')
      expect(child2.table_name).to eql('activity_books')
      expect(other.table_name).to eql('authors')
    end

    it 'respects the table name prefix and suffix defined on parent module' do
      mod = Object.const_set('Private', Module.new)
      mod.define_singleton_method(:table_name_prefix) { 'private.' }
      mod.define_singleton_method(:table_name_suffix) { '_bundle' }
      result = 'private.activity_post_others_bundle'

      klass = mod.const_set('Other', Class.new(ActivityPost))
      allow(klass).to receive(:module_parent).and_return(child)
      allow(klass).to receive(:module_parents).and_return([mod])
      allow(klass).to receive(:physically_inherited?).and_return(true)
      expect(klass.send(:compute_table_name)).to be_eql(result)
    end
  end

  context 'on relation' do
    let(:base) { Activity }
    let(:child) { ActivityBook }
    let(:other) { AuthorJournalist }

    it 'has operation methods' do
      expect(base).to respond_to(:itself_only)
      expect(base).to respond_to(:cast_records)
      expect(base.new).to respond_to(:cast_record)
    end

    context 'itself only' do
      it 'does not mess with original queries' do
        expect(base.all.to_sql).to \
          eql('SELECT "activities".* FROM "activities"')
      end

      it 'adds the only condition to the query' do
        expect(base.itself_only.to_sql).to \
          eql('SELECT "activities".* FROM ONLY "activities"')
      end

      it 'returns the right ammount of entries' do
        base.create!(title: 'Activity only')
        child.create!(title: 'Activity book')

        expect(base.count).to eql(2)
        expect(base.itself_only.count).to eql(1)
        expect(child.count).to eql(1)
      end
    end

    context 'cast records' do
      before :each do
        base.create(title: 'Activity test')
        child.create(title: 'Activity book', url: 'bookurl1')
        other.create(name: 'An author name')
      end

      it 'does not mess with single table inheritance' do
        result = 'SELECT "authors".* FROM "authors"'
        result << " WHERE \"authors\".\"type\" = 'AuthorJournalist'"
        expect(other.all.to_sql).to eql(result)
      end

      it 'adds all statements to load all the necessary records' do
        result = 'SELECT "activities".*, "activities"."tableoid"::regclass AS _record_class, "i_0"."description"'
        result << ', COALESCE("i_0"."url", "i_1"."url", "i_2"."url") AS url, "i_0"."activated" AS activity_books__activated'
        result << ', "i_1"."activated" AS activity_posts__activated, "i_2"."activated" AS activity_post_samples__activated'
        result << ', COALESCE("i_1"."file", "i_2"."file") AS file, COALESCE("i_1"."post_id", "i_2"."post_id") AS post_id'
        result << ", \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books', 'activity_posts', 'activity_post_samples') AS _auto_cast"
        result << ' FROM "activities"'
        result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
        result << ' LEFT OUTER JOIN "activity_posts" "i_1" ON "activities"."id" = "i_1"."id"'
        result << ' LEFT OUTER JOIN "activity_post_samples" "i_2" ON "activities"."id" = "i_2"."id"'
        expect(base.cast_records.all.to_sql).to eql(result)
      end

      it 'can be have simplefied joins' do
        result = 'SELECT "activities".*, "activities"."tableoid"::regclass AS _record_class'
        result << ', "i_0"."description", "i_0"."url", "i_0"."activated"'
        result << ", \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books') AS _auto_cast"
        result << ' FROM "activities"'
        result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
        expect(base.cast_records(child).all.to_sql).to eql(result)
      end

      it 'can be filtered by record type' do
        result = 'SELECT "activities".*, "activities"."tableoid"::regclass AS _record_class'
        result << ', "i_0"."description", "i_0"."url", "i_0"."activated"'
        result << ", \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books') AS _auto_cast"
        result << ' FROM "activities"'
        result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
        result << " WHERE \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books')"
        expect(base.cast_records(child, filter: true).all.to_sql).to eql(result)
      end

      it 'works with count and does not add extra columns' do
        result = 'SELECT COUNT(*)'
        result << ' FROM "activities"'
        result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
        result << ' LEFT OUTER JOIN "activity_posts" "i_1" ON "activities"."id" = "i_1"."id"'
        result << ' LEFT OUTER JOIN "activity_post_samples" "i_2" ON "activities"."id" = "i_2"."id"'
        query = get_last_executed_query{ base.cast_records.all.count }
        expect(query).to eql(result)
      end

      it 'works with sum and does not add extra columns' do
        result = 'SELECT SUM("activities"."id")'
        result << ' FROM "activities"'
        result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
        result << ' LEFT OUTER JOIN "activity_posts" "i_1" ON "activities"."id" = "i_1"."id"'
        result << ' LEFT OUTER JOIN "activity_post_samples" "i_2" ON "activities"."id" = "i_2"."id"'
        query = get_last_executed_query{ base.cast_records.all.sum(:id) }
        expect(query).to eql(result)
      end

      it 'returns the correct model object' do
        ActivityPost.create(title: 'Activity post')
        ActivityPost::Sample.create(title: 'Activity post')
        records = base.cast_records.order(:id).load.to_a

        expect(records[0]).to be_instance_of(Activity)
        expect(records[1]).to be_instance_of(ActivityBook)
        expect(records[2]).to be_instance_of(ActivityPost)
        expect(records[3]).to be_instance_of(ActivityPost::Sample)
      end

      it 'does not cast unnecessary records' do
        ActivityPost.create(title: 'Activity post')
        records = base.cast_records(ActivityBook).order(:id).load.to_a

        expect(records[0]).to be_instance_of(Activity)
        expect(records[1]).to be_instance_of(ActivityBook)
        expect(records[2]).to be_instance_of(Activity)
      end

      it 'correctly identifies same name attributes' do
        ActivityPost.create(title: 'Activity post', url: 'posturl1')
        records = base.cast_records.order(:id).load.to_a

        expect(records[1].url).to eql('bookurl1')
        expect(records[2].url).to eql('posturl1')
      end

      # TODO: Maybe in the future
      xit 'does not make internal inheritance attributes accessible' do
        record = base.cast_records.order(:id).load.last

        expect(record).to be_instance_of(ActivityBook)
        expect(record).not_to respond_to(:_record_class)
        expect(record).not_to respond_to(:_auto_cast)
      end
    end

    context 'cast record' do
      before :each do
        base.create(title: 'Activity test')
        child.create(title: 'Activity book')
        other.create(name: 'An author name')
        base.instance_variable_set(:@casted_dependents, nil)
      end

      it 'does not affect normal records' do
        expect(base.first.cast_record).to be_a(base)
        expect(child.first.cast_record).to be_a(child)
        expect(other.first.cast_record).to be_a(other)
      end

      it 'rises an error when the casted model cannot be defined' do
        base.instance_variable_set(:@casted_dependents, {})
        expect{ base.second.cast_record }.to raise_error(ArgumentError, /to type 'activity_books'/)
      end

      it 'can return the record class even when the auxiliary statement is not mentioned' do
        expect(base.first._record_class).to eql('activities')
        expect(base.second._record_class).to eql('activity_books')
        expect(other.first._record_class).to eql('authors')
      end

      it 'does trigger record casting when accessed through inheritance' do
        expect(base.second.cast_record).to eql(child.first)
      end

      context 'using uuid' do
        let(:base) { Question }
        let(:child) { QuestionSelect }

        before :each do
          base.create(title: 'Simple question')
          child.create(title: 'Select question')
          base.instance_variable_set(:@casted_dependents, nil)
        end

        it 'does not affect normal records' do
          expect(base.first.cast_record).to be_a(base)
          expect(child.first.cast_record).to be_a(child)
        end

        it 'does trigger record casting when accessed through inheritance' do
          expect(base.second.cast_record).to eql(child.first)
        end
      end
    end
  end
end
