require 'spec_helper'

describe ActiveRecordViews::Extension do
  describe '.is_view' do
    it 'creates database views from heredocs' do
      expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
      expect(HeredocTestModel.first.name).to eq 'Here document'
    end

    it 'creates database views from external SQL files' do
      expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
      expect(ExternalFileTestModel.first.name).to eq 'External SQL file'
    end

    it 'creates database views from namespaced external SQL files' do
      expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
      expect(Namespace::TestModel.first.name).to eq 'Namespaced SQL file'
    end

    it 'creates database views from external ERB files' do
      expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
      expect(ErbTestModel.first.name).to eq 'ERB file'
    end

    it 'errors if external SQL file is missing' do
      expect {
        MissingFileTestModel
      }.to raise_error RuntimeError, /could not find missing_file_test_model.sql/
    end

    it 'reloads the database view when external SQL file is modified' do
      %w[foo bar baz].each do |sql|
        expect(ActiveRecordViews).to receive(:create_view).with(
          anything,
          'modified_file_test_models',
          'ModifiedFileTestModel',
          sql,
          {}
        ).once.ordered
      end

      with_temp_sql_dir do |temp_dir|
        sql_file = File.join(temp_dir, 'modified_file_test_model.sql')
        update_file sql_file, 'foo'

        class ModifiedFileTestModel < ActiveRecord::Base
          is_view
        end

        update_file sql_file, 'bar'

        test_request
        test_request # second request does not `create_view` again

        update_file sql_file, 'baz'

        test_request
      end
      test_request # trigger cleanup
    end

    it 'drops the view if the external SQL file is deleted' do
      with_temp_sql_dir do |temp_dir|
        sql_file = File.join(temp_dir, 'deleted_file_test_model.sql')
        File.write sql_file, "SELECT 1 AS id, 'delete test'::text AS name"

        class DeletedFileTestModel < ActiveRecord::Base
          is_view
        end

        expect(DeletedFileTestModel.first.name).to eq 'delete test'

        File.unlink sql_file

        expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ADROP/).once.and_call_original
        test_request
        test_request # second request does not `drop_view` again

        expect {
          DeletedFileTestModel.first.name
        }.to raise_error ActiveRecord::StatementInvalid, /relation "deleted_file_test_models" does not exist/
      end
    end

    it 'does not create if database view is initially up to date' do
      ActiveRecordViews.create_view ActiveRecord::Base.connection, 'initial_create_test_models', 'InitialCreateTestModel', 'SELECT 42 as id'
      expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ACREATE (?:OR REPLACE )?VIEW/).never
      class InitialCreateTestModel < ActiveRecord::Base
        is_view 'SELECT 42 as id'
      end
    end

    it 'successfully recreates modified paired views with incompatible changes' do
      ActiveRecordViews.create_view ActiveRecord::Base.connection, 'modified_as', 'ModifiedA', 'SELECT 11 AS old_name;'
      ActiveRecordViews.create_view ActiveRecord::Base.connection, 'modified_bs', 'ModifiedB', 'SELECT old_name FROM modified_as;'

      expect(ModifiedB.first.attributes.except(nil)).to eq('new_name' => 22)
    end

    it 'successfully restores dependant view when temporarily dropping dependency' do
      ActiveRecordViews.create_view ActiveRecord::Base.connection, 'dependency_as', 'DependencyA', 'SELECT 42 AS foo, 1 AS id;'
      ActiveRecordViews.create_view ActiveRecord::Base.connection, 'dependency_bs', 'DependencyB', 'SELECT * FROM dependency_as;'

      expect(DependencyA.first.id).to eq 2
      expect(DependencyB.first.id).to eq 2
    end

    it 'errors if more than one argument is specified' do
      expect {
        class TooManyArguments < ActiveRecord::Base
          is_view 'SELECT 1 AS ID;', 'SELECT 2 AS ID;'
        end
      }.to raise_error ArgumentError, 'wrong number of arguments (2 for 0..1)'
    end

    it 'errors if an invalid option is specified' do
      expect {
        class InvalidOption < ActiveRecord::Base
          is_view 'SELECT 1 AS ID;', blargh: 123
        end
      }.to raise_error ArgumentError, /^Unknown key: :?blargh/
    end

    it 'creates/refreshes/drops materialized views' do
      with_temp_sql_dir do |temp_dir|
        sql_file = File.join(temp_dir, 'materialized_view_test_model.sql')
        File.write sql_file, 'SELECT 123 AS id;'

        class MaterializedViewTestModel < ActiveRecord::Base
          is_view materialized: true
        end

        expect {
          MaterializedViewTestModel.first!
        }.to raise_error ActiveRecord::StatementInvalid, /materialized view "materialized_view_test_models" has not been populated/

        expect(MaterializedViewTestModel.view_populated?).to eq false
        MaterializedViewTestModel.refresh_view!
        expect(MaterializedViewTestModel.view_populated?).to eq true

        expect(MaterializedViewTestModel.first!.id).to eq 123

        File.unlink sql_file
        test_request

        expect {
          MaterializedViewTestModel.first!
        }.to raise_error ActiveRecord::StatementInvalid, /relation "materialized_view_test_models" does not exist/
      end
    end

    it 'raises an error for `view_populated?` if view is not materialized' do
      class NonMaterializedViewPopulatedTestModel < ActiveRecord::Base
        is_view 'SELECT 1 AS id;'
      end

      expect {
        NonMaterializedViewPopulatedTestModel.view_populated?
      }.to raise_error ArgumentError, 'not a materialized view'
    end

    it 'supports refreshing materialized views concurrently' do
      class MaterializedViewRefreshTestModel < ActiveRecord::Base
        is_view 'SELECT 1 AS id;', materialized: true
      end
      class MaterializedViewConcurrentRefreshTestModel < ActiveRecord::Base
        is_view 'SELECT 1 AS id;', materialized: true, unique_columns: [:id]
      end
      MaterializedViewConcurrentRefreshTestModel.refresh_view!

      expect(ActiveRecord::Base.connection).to receive(:execute).with('REFRESH MATERIALIZED VIEW "materialized_view_refresh_test_models";').once.and_call_original
      expect(ActiveRecord::Base.connection).to receive(:execute).with('REFRESH MATERIALIZED VIEW CONCURRENTLY "materialized_view_concurrent_refresh_test_models";').once.and_call_original

      MaterializedViewRefreshTestModel.refresh_view!
      MaterializedViewConcurrentRefreshTestModel.refresh_view! concurrent: true
    end

    it 'supports opportunistically refreshing materialized views concurrently' do
      class MaterializedViewAutoRefreshTestModel < ActiveRecord::Base
        is_view 'SELECT 1 AS id;', materialized: true, unique_columns: [:id]
      end

      expect(ActiveRecord::Base.connection).to receive(:execute).with('REFRESH MATERIALIZED VIEW "materialized_view_auto_refresh_test_models";').once.and_call_original
      expect(ActiveRecord::Base.connection).to receive(:execute).with('REFRESH MATERIALIZED VIEW CONCURRENTLY "materialized_view_auto_refresh_test_models";').once.and_call_original

      MaterializedViewAutoRefreshTestModel.refresh_view! concurrent: :auto
      MaterializedViewAutoRefreshTestModel.refresh_view! concurrent: :auto
    end

    it 'raises an error when refreshing materialized views with invalid concurrent option' do
      class MaterializedViewInvalidRefreshTestModel < ActiveRecord::Base
        is_view 'SELECT 1 AS id;', materialized: true, unique_columns: [:id]
      end

      expect {
        MaterializedViewAutoRefreshTestModel.refresh_view! concurrent: :blah
      }.to raise_error ArgumentError, 'invalid concurrent option'
    end
  end
end