# frozen_string_literal: true

describe "OracleEnhancedAdapter" do
  include LoggerSpecHelper
  include SchemaSpecHelper

  before(:all) do
    ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
  end

  describe "cache table columns" do
    before(:all) do
      @conn = ActiveRecord::Base.connection
      schema_define do
        create_table :test_employees, force: true do |t|
          t.string  :first_name, limit: 20
          t.string  :last_name, limit: 25
          if ActiveRecord::Base.connection.supports_virtual_columns?
            t.virtual :full_name, as: "(first_name || ' ' || last_name)"
          else
            t.string  :full_name, limit: 46
          end
          t.date    :hire_date
        end
      end
      schema_define do
        create_table :test_employees_without_pk, id: false, force: true do |t|
          t.string  :first_name, limit: 20
          t.string  :last_name, limit: 25
          t.date    :hire_date
        end
      end
      @column_names = ["id", "first_name", "last_name", "full_name", "hire_date"]
      @column_sql_types = ["NUMBER(38)", "VARCHAR2(20)", "VARCHAR2(25)", "VARCHAR2(46)", "DATE"]
      class ::TestEmployee < ActiveRecord::Base
      end
      # Another class using the same table
      class ::TestEmployee2 < ActiveRecord::Base
        self.table_name = "test_employees"
      end
    end

    after(:all) do
      @conn = ActiveRecord::Base.connection
      Object.send(:remove_const, "TestEmployee")
      Object.send(:remove_const, "TestEmployee2")
      @conn.drop_table :test_employees, if_exists: true
      @conn.drop_table :test_employees_without_pk, if_exists: true
      ActiveRecord::Base.clear_cache!
    end

    before(:each) do
      set_logger
      @conn = ActiveRecord::Base.connection
    end

    after(:each) do
      clear_logger
    end

    describe "without column caching" do
      it "should identify virtual columns as such" do
        skip "Not supported in this database version" unless @conn.supports_virtual_columns?
        te = TestEmployee.connection.columns("test_employees").detect(&:virtual?)
        expect(te.name).to eq("full_name")
      end

      it "should get columns from database at first time" do
        @conn.clear_table_columns_cache(:test_employees)
        expect(TestEmployee.connection.columns("test_employees").map(&:name)).to eq(@column_names)
        expect(@logger.logged(:debug).last).to match(/select .* from all_tab_cols/im)
      end

      it "should not get columns from database at second time" do
        TestEmployee.connection.columns("test_employees")
        @logger.clear(:debug)
        expect(TestEmployee.connection.columns("test_employees").map(&:name)).to eq(@column_names)
        expect(@logger.logged(:debug).last).not_to match(/select .* from all_tab_cols/im)
      end

      it "should get primary key from database at first time" do
        expect(TestEmployee.connection.pk_and_sequence_for("test_employees")).to eq(["id", "test_employees_seq"])
        expect(@logger.logged(:debug).last).to match(/select .* from all_constraints/im)
      end

      it "should get primary key from database at first time" do
        expect(TestEmployee.connection.pk_and_sequence_for("test_employees")).to eq(["id", "test_employees_seq"])
        @logger.clear(:debug)
        expect(TestEmployee.connection.pk_and_sequence_for("test_employees")).to eq(["id", "test_employees_seq"])
        expect(@logger.logged(:debug).last).to match(/select .* from all_constraints/im)
      end

      it "should have correct sql types when 2 models are using the same table and AR query cache is enabled" do
        @conn.cache do
          expect(TestEmployee.columns.map(&:sql_type)).to eq(@column_sql_types)
          expect(TestEmployee2.columns.map(&:sql_type)).to eq(@column_sql_types)
        end
      end

      it "should get sequence value at next time" do
        TestEmployee.create!
        expect(@logger.logged(:debug).first).not_to match(/SELECT "TEST_EMPLOYEES_SEQ".NEXTVAL FROM dual/im)
        @logger.clear(:debug)
        TestEmployee.create!
        expect(@logger.logged(:debug).first).to match(/SELECT "TEST_EMPLOYEES_SEQ".NEXTVAL FROM dual/im)
      end
    end
  end

  describe "session information" do
    before(:all) do
      @conn = ActiveRecord::Base.connection
    end

    it "should get current database name" do
      # get database name if using //host:port/database connection string
      database_name = CONNECTION_PARAMS[:database].split("/").last
      expect(@conn.current_database.upcase).to eq(database_name.upcase)
    end

    it "should get current database session user" do
      expect(@conn.current_user.upcase).to eq(CONNECTION_PARAMS[:username].upcase)
    end
  end

  describe "temporary tables" do
    before(:all) do
      ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[:table] = "UNUSED"
      ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[:clob] = "UNUSED"
      @conn = ActiveRecord::Base.connection
    end
    after(:all) do
      ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces = {}
    end

    after(:each) do
      @conn.drop_table :foos, if_exists: true
    end
    it "should create ok" do
      @conn.create_table :foos, temporary: true, id: false do |t|
        t.integer :id
        t.text :bar
      end
    end
    it "should show up as temporary" do
      @conn.create_table :foos, temporary: true, id: false do |t|
        t.integer :id
      end
      expect(@conn.temporary_table?("foos")).to be_truthy
    end
  end

  describe "`has_many` assoc has `dependent: :delete_all` with `order`" do
    before(:all) do
      schema_define do
        create_table :test_posts do |t|
          t.string      :title
        end
        create_table :test_comments do |t|
          t.integer     :test_post_id
          t.string      :description
        end
        add_index :test_comments, :test_post_id
      end
      class ::TestPost < ActiveRecord::Base
        has_many :test_comments, -> { order(:id) }, dependent: :delete_all
      end
      class ::TestComment < ActiveRecord::Base
        belongs_to :test_post
      end
      TestPost.transaction do
        post = TestPost.create!(title: "Title")
        TestComment.create!(test_post_id: post.id, description: "Description")
      end
    end

    after(:all) do
      schema_define do
        drop_table :test_comments
        drop_table :test_posts
      end
      Object.send(:remove_const, "TestPost")
      Object.send(:remove_const, "TestComment")
      ActiveRecord::Base.clear_cache!
    end

    it "should not occur `ActiveRecord::StatementInvalid: OCIError: ORA-00907: missing right parenthesis`" do
      expect { TestPost.first.destroy }.not_to raise_error
    end
  end

  describe "eager loading" do
    before(:all) do
      schema_define do
        create_table :test_posts do |t|
          t.string      :title
        end
        create_table :test_comments do |t|
          t.integer     :test_post_id
          t.string      :description
        end
        add_index :test_comments, :test_post_id
      end
      class ::TestPost < ActiveRecord::Base
        has_many :test_comments
      end
      class ::TestComment < ActiveRecord::Base
        belongs_to :test_post
      end
      @ids = (1..1010).to_a
      TestPost.transaction do
        @ids.each do |id|
          TestPost.create!(id: id, title: "Title #{id}")
          TestComment.create!(test_post_id: id, description: "Description #{id}")
        end
      end
    end

    after(:all) do
      schema_define do
        drop_table :test_comments
        drop_table :test_posts
      end
      Object.send(:remove_const, "TestPost")
      Object.send(:remove_const, "TestComment")
      ActiveRecord::Base.clear_cache!
    end

    it "should load included association with more than 1000 records" do
      posts = TestPost.includes(:test_comments).to_a
      expect(posts.size).to eq(@ids.size)
    end
  end

  describe "lists" do
    before(:all) do
      schema_define do
        create_table :test_posts do |t|
          t.string :title
        end
      end
      class ::TestPost < ActiveRecord::Base
        has_many :test_comments
      end
      @ids = (1..1010).to_a
      TestPost.transaction do
        @ids.each do |id|
          TestPost.create!(id: id, title: "Title #{id}")
        end
      end
    end

    after(:all) do
      schema_define do
        drop_table :test_posts
      end
      Object.send(:remove_const, "TestPost")
      ActiveRecord::Base.clear_cache!
    end

    ##
    # See this GitHub issue for an explanation of homogenous lists.
    # https://github.com/rails/rails/commit/72fd0bae5948c1169411941aeea6fef4c58f34a9
    it "should allow more than 1000 items in a list where the list is homogenous" do
      posts = TestPost.where(id: @ids).to_a
      expect(posts.size).to eq(@ids.size)
    end

    it "should allow more than 1000 items in a list where the list is non-homogenous" do
      posts = TestPost.where(id: [*@ids, nil]).to_a
      expect(posts.size).to eq(@ids.size)
    end
  end

  describe "with statement pool" do
    before(:all) do
      ActiveRecord::Base.establish_connection(CONNECTION_PARAMS.merge(statement_limit: 3))
      @conn = ActiveRecord::Base.connection
      schema_define do
        drop_table :test_posts, if_exists: true
        create_table :test_posts
      end
      class ::TestPost < ActiveRecord::Base
      end
      @statements = @conn.instance_variable_get(:@statements)
    end

    before(:each) do
      @conn.clear_cache!
    end

    after(:all) do
      schema_define do
        drop_table :test_posts
      end
      Object.send(:remove_const, "TestPost")
      ActiveRecord::Base.clear_cache!
    end

    it "should clear older cursors when statement limit is reached" do
      binds = [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::OracleEnhanced::Integer.new)]
      # free statement pool from dictionary selections  to ensure next selects will increase statement pool
      @statements.clear
      expect {
        4.times do |i|
          @conn.exec_query("SELECT * FROM test_posts WHERE #{i}=#{i} AND id = :id", "SQL", binds)
        end
      }.to change(@statements, :length).by(+3)
    end

    it "should cache UPDATE statements with bind variables" do
      expect {
        binds = [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::OracleEnhanced::Integer.new)]
        @conn.exec_update("UPDATE test_posts SET id = :id", "SQL", binds)
      }.to change(@statements, :length).by(+1)
    end

    it "should not cache UPDATE statements without bind variables" do
      expect {
        binds = []
        @conn.exec_update("UPDATE test_posts SET id = 1", "SQL", binds)
      }.not_to change(@statements, :length)
    end
  end

  describe "database_exists?" do
    it "should raise `NotImplementedError`" do
      expect {
        ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.database_exists?(CONNECTION_PARAMS)
      }.to raise_error(NotImplementedError)
    end
  end

  describe "explain" do
    before(:all) do
      @conn = ActiveRecord::Base.connection
      schema_define do
        drop_table :test_posts, if_exists: true
        create_table :test_posts
      end
      class ::TestPost < ActiveRecord::Base
      end
    end

    after(:all) do
      schema_define do
        drop_table :test_posts
      end
      Object.send(:remove_const, "TestPost")
      ActiveRecord::Base.clear_cache!
    end

    it "should explain query" do
      explain = TestPost.where(id: 1).explain
      expect(explain).to include("Cost")
      expect(explain).to include("INDEX UNIQUE SCAN")
    end

    it "should explain query with binds" do
      binds = [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::OracleEnhanced::Integer.new)]
      explain = TestPost.where(id: binds).explain
      expect(explain).to include("Cost")
      expect(explain).to include("INDEX UNIQUE SCAN").or include("TABLE ACCESS FULL")
    end
  end

  describe "using offset and limit" do
    before(:all) do
      @conn = ActiveRecord::Base.connection
      schema_define do
        create_table :test_employees, force: true do |t|
          t.integer   :sort_order
          t.string    :first_name, limit: 20
          t.string    :last_name, limit: 20
          t.timestamps
        end
      end
      @employee = Class.new(ActiveRecord::Base) do
        self.table_name = :test_employees
      end
      @employee.create!(sort_order: 1, first_name: "Peter",   last_name: "Parker")
      @employee.create!(sort_order: 2, first_name: "Tony",    last_name: "Stark")
      @employee.create!(sort_order: 3, first_name: "Steven",  last_name: "Rogers")
      @employee.create!(sort_order: 4, first_name: "Bruce",   last_name: "Banner")
      @employee.create!(sort_order: 5, first_name: "Natasha", last_name: "Romanova")
    end

    after(:all) do
      @conn.drop_table :test_employees, if_exists: true
    end

    after(:each) do
      ActiveRecord::Base.clear_cache!
    end

    it "should return n records with limit(n)" do
      expect(@employee.limit(3).to_a.size).to be(3)
    end

    it "should return less than n records with limit(n) if there exist less than n records" do
      expect(@employee.limit(10).to_a.size).to be(5)
    end

    it "should return the records starting from offset n with offset(n)" do
      expect(@employee.order(:sort_order).first.first_name).to eq("Peter")
      expect(@employee.order(:sort_order).offset(0).first.first_name).to eq("Peter")
      expect(@employee.order(:sort_order).offset(1).first.first_name).to eq("Tony")
      expect(@employee.order(:sort_order).offset(4).first.first_name).to eq("Natasha")
    end
  end

  describe "valid_type?" do
    before(:all) do
      @conn = ActiveRecord::Base.connection
      schema_define do
        create_table :test_employees, force: true do |t|
          t.string :first_name, limit: 20
        end
      end
    end

    after(:all) do
      @conn.drop_table :test_employees, if_exists: true
    end

    it "returns true when passed a valid type" do
      column = @conn.columns("test_employees").find { |col| col.name == "first_name" }
      expect(@conn.valid_type?(column.type)).to be true
    end

    it "returns false when passed an invalid type" do
      expect(@conn.valid_type?(:foobar)).to be false
    end
  end

  describe "serialized column" do
    before(:all) do
      schema_define do
        create_table :test_serialized_columns do |t|
          t.text :serialized
        end
      end
      class ::TestSerializedColumn < ActiveRecord::Base
        serialize :serialized, Array
      end
    end

    after(:all) do
      schema_define do
        drop_table :test_serialized_columns
      end
      Object.send(:remove_const, "TestSerializedColumn")
      ActiveRecord::Base.table_name_prefix = nil
      ActiveRecord::Base.clear_cache!
    end

    before(:each) do
      set_logger
    end

    after(:each) do
      clear_logger
    end

    it "should serialize" do
      new_value = "new_value"
      serialized_column = TestSerializedColumn.new

      expect(serialized_column.serialized).to eq([])
      serialized_column.serialized << new_value
      expect(serialized_column.serialized).to eq([new_value])
      serialized_column.save
      expect(serialized_column.save!).to eq(true)

      serialized_column.reload
      expect(serialized_column.serialized).to eq([new_value])
      serialized_column.serialized = []
      expect(serialized_column.save!).to eq(true)
    end
  end

  describe "Binary lob column" do
    before(:all) do
      schema_define do
        create_table :test_binary_columns do |t|
          t.binary :attachment
        end
      end
      class ::TestBinaryColumn < ActiveRecord::Base
      end
    end

    after(:all) do
      schema_define do
        drop_table :test_binary_columns
      end
      Object.send(:remove_const, "TestBinaryColumn")
      ActiveRecord::Base.table_name_prefix = nil
      ActiveRecord::Base.clear_cache!
    end

    before(:each) do
      set_logger
    end

    after(:each) do
      clear_logger
    end

    it "should serialize with non UTF-8 data" do
      binary_value = +"Hello \x93\xfa\x96\x7b"
      binary_value.force_encoding "UTF-8"

      binary_column_object = TestBinaryColumn.new
      binary_column_object.attachment = binary_value

      expect(binary_column_object.save!).to eq(true)
    end
  end

  describe "quoting" do
    before(:all) do
      schema_define do
        create_table :test_logs, force: true do |t|
          t.timestamp :send_time
        end
      end
      class TestLog < ActiveRecord::Base
        validates_uniqueness_of :send_time
      end
    end

    after(:all) do
      schema_define do
        drop_table :test_logs
      end
      Object.send(:remove_const, "TestLog")
      ActiveRecord::Base.clear_cache! if ActiveRecord::Base.respond_to?(:"clear_cache!")
    end

    it "should create records including Time"  do
      TestLog.create! send_time: Time.now + 1.seconds
      TestLog.create! send_time: Time.now + 2.seconds
      expect(TestLog.count).to eq 2
    end
  end

  describe "synonym_names" do
    before(:all) do
      schema_define do
        create_table :test_comments, force: true do |t|
          t.string :comment
        end
        add_synonym :synonym_comments, :test_comments
      end
    end

    after(:all) do
      schema_define do
        drop_table :test_comments
        remove_synonym :synonym_comments
      end
      ActiveRecord::Base.clear_cache! if ActiveRecord::Base.respond_to?(:"clear_cache!")
    end

    it "includes synonyms in data_source" do
      conn = ActiveRecord::Base.connection
      expect(conn).to be_data_source_exist("synonym_comments")
      expect(conn.data_sources).to include("synonym_comments")
    end
  end

  describe "dictionary selects with bind variables" do
    before(:all) do
      ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
      @conn = ActiveRecord::Base.connection
      schema_define do
        drop_table :test_posts, if_exists: true
        create_table :test_posts

        drop_table :users, if_exists: true
        create_table :users, force: true do |t|
          t.string :name
          t.integer :group_id
        end

        drop_table :groups, if_exists: true
        create_table :groups, force: true do |t|
          t.string :name
        end
      end

      class ::TestPost < ActiveRecord::Base
      end

      class User < ActiveRecord::Base
        belongs_to :group
      end

      class Group < ActiveRecord::Base
        has_one :user
      end
    end

    before(:each) do
      @conn.clear_cache!
      set_logger
    end

    after(:each) do
      clear_logger
    end

    after(:all) do
      schema_define do
        drop_table :test_posts
        drop_table :users
        drop_table :groups
      end
      Object.send(:remove_const, "TestPost")
      ActiveRecord::Base.clear_cache!
    end

    it "should test table existence" do
      expect(@conn.table_exists?("TEST_POSTS")).to eq true
      expect(@conn.table_exists?("NOT_EXISTING")).to eq false
    end

    it "should return array from indexes with bind usage" do
       expect(@conn.indexes("TEST_POSTS").class).to eq Array
       expect(@logger.logged(:debug).last).to match(/:table_name/)
       expect(@logger.logged(:debug).last).to match(/\["table_name", "TEST_POSTS"\]/)
     end

    it "should return content from columns witt bind usage" do
      expect(@conn.columns("TEST_POSTS").length).to be > 0
      expect(@logger.logged(:debug).last).to match(/:table_name/)
      expect(@logger.logged(:debug).last).to match(/\["table_name", "TEST_POSTS"\]/)
    end

    it "should return pk and sequence from pk_and_sequence_for with bind usage" do
      expect(@conn.pk_and_sequence_for("TEST_POSTS").length).to eq 2
      expect(@logger.logged(:debug).last).to match(/\["table_name", "TEST_POSTS"\]/)
    end

    it "should return pk from primary_keys with bind usage" do
      expect(@conn.primary_keys("TEST_POSTS")).to eq ["id"]
      expect(@logger.logged(:debug).last).to match(/\["table_name", "TEST_POSTS"\]/)
    end

    it "should not raise missing IN/OUT parameter like issue 1687 " do
      # "to_sql" enforces unprepared_statement including dictionary access SQLs
      expect { User.joins(:group).to_sql }.not_to raise_exception
    end

    it "should return false from temporary_table? with bind usage" do
      expect(@conn.temporary_table?("TEST_POSTS")).to eq false
      expect(@logger.logged(:debug).last).to match(/:table_name/)
      expect(@logger.logged(:debug).last).to match(/\["table_name", "TEST_POSTS"\]/)
    end
  end

  describe "Transaction" do
    before(:all) do
      schema_define do
        create_table :test_posts do |t|
          t.string :title
        end
      end
      class ::TestPost < ActiveRecord::Base
      end
      Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception
    end

    it "Raises Deadlocked when a deadlock is encountered" do
      skip "Skip temporary due to #1599" if ActiveRecord::Base.connection.supports_fetch_first_n_rows_and_offset?
      expect {
        barrier = Concurrent::CyclicBarrier.new(2)

        t1 = TestPost.create(title: "one")
        t2 = TestPost.create(title: "two")

        thread = Thread.new do
          TestPost.transaction do
            t1.lock!
            barrier.wait
            t2.update(title: "one")
          end
        end

        begin
          TestPost.transaction do
            t2.lock!
            barrier.wait
            t1.update(title: "two")
          end
        ensure
          thread.join
        end
      }.to raise_error(ActiveRecord::Deadlocked)
    end

    after(:all) do
      schema_define do
        drop_table :test_posts
      end
      Object.send(:remove_const, "TestPost") rescue nil
      ActiveRecord::Base.clear_cache!
      Thread.report_on_exception = @original_report_on_exception
    end
  end

  describe "Sequence" do
    before(:all) do
      ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
      @conn = ActiveRecord::Base.connection
      schema_define do
        create_table :table_with_name_thats_just_ok,
          sequence_name: "suitably_short_seq", force: true do |t|
          t.column :foo, :string, null: false
        end
      end
    end
    after(:all) do
      schema_define do
        drop_table :table_with_name_thats_just_ok,
          sequence_name: "suitably_short_seq" rescue nil
      end
    end
    it "should create table with custom sequence name" do
      expect(@conn.select_value("select suitably_short_seq.nextval from dual")).to eq(1)
    end
  end

  describe "Hints" do
    before(:all) do
      ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
      @conn = ActiveRecord::Base.connection
      schema_define do
        drop_table :test_posts, if_exists: true
        create_table :test_posts
      end
      class ::TestPost < ActiveRecord::Base
      end
    end

    before(:each) do
      @conn.clear_cache!
      set_logger
    end

    after(:each) do
      clear_logger
    end

    after(:all) do
      schema_define do
        drop_table :test_posts
      end
      Object.send(:remove_const, "TestPost")
      ActiveRecord::Base.clear_cache!
    end

    it "should explain considers hints" do
      post = TestPost.optimizer_hints("FULL (\"TEST_POSTS\")")
      post = post.where(id: 1)
      expect(post.explain).to include("|  TABLE ACCESS FULL| TEST_POSTS |")
    end

    it "should explain considers hints with /*+ */ " do
      post = TestPost.optimizer_hints("/*+ FULL (\"TEST_POSTS\") */")
      post = post.where(id: 1)
      expect(post.explain).to include("|  TABLE ACCESS FULL| TEST_POSTS |")
    end
  end

  describe "homogeneous in" do
    before(:all) do
      ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
      @conn = ActiveRecord::Base.connection
      schema_define do
        create_table :test_posts, force: true
        create_table :test_comments, force: true do |t|
          t.integer :test_post_id
        end
      end
      class ::TestPost < ActiveRecord::Base
        has_many :test_comments
      end
      class ::TestComment < ActiveRecord::Base
        belongs_to :test_post
      end
    end

    after(:all) do
      schema_define do
        drop_table :test_posts, if_exists: true
        drop_table :test_comments, if_exists: true
      end
      Object.send(:remove_const, "TestPost")
      Object.send(:remove_const, "TestComment")
      ActiveRecord::Base.clear_cache!
    end

    it "should not raise undefined method length" do
      post = TestPost.create!
      post.test_comments << TestComment.create!
      expect(TestComment.where(test_post_id: TestPost.select(:id)).size).to eq(1)
    end
  end
end