# frozen_string_literal: true

require "cases/helper"
require "models/owner"
require "tempfile"
require "support/ddl_helper"

module ActiveRecord
  module ConnectionAdapters
    class SQLite3AdapterTest < ActiveRecord::SQLite3TestCase
      include DdlHelper

      self.use_transactional_tests = false

      class DualEncoding < ActiveRecord::Base
      end

      def setup
        @conn = Base.sqlite3_connection database: ":memory:",
                                        adapter: "sqlite3",
                                        timeout: 100

        @connection_handler = ActiveRecord::Base.connection_handler
      end

      def test_bad_connection
        assert_raise ActiveRecord::NoDatabaseError do
          connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db")
          connection.drop_table "ex", if_exists: true
        end
      end

      def test_database_exists_returns_false_when_the_database_does_not_exist
        assert_not SQLite3Adapter.database_exists?(adapter: "sqlite3", database: "non_extant_db"),
          "expected non_extant_db to not exist"
      end

      def test_database_exists_returns_true_when_database_exists
        db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
        assert SQLite3Adapter.database_exists?(db_config.configuration_hash),
          "expected #{db_config.database} to exist"
      end

      unless in_memory_db?
        def test_connect_with_url
          original_connection = ActiveRecord::Base.remove_connection
          tf = Tempfile.open "whatever"
          url = "sqlite3:#{tf.path}"
          ActiveRecord::Base.establish_connection(url)
          assert ActiveRecord::Base.connection
        ensure
          tf.close
          tf.unlink
          ActiveRecord::Base.establish_connection(original_connection)
        end

        def test_connect_memory_with_url
          original_connection = ActiveRecord::Base.remove_connection
          url = "sqlite3::memory:"
          ActiveRecord::Base.establish_connection(url)
          assert ActiveRecord::Base.connection
        ensure
          ActiveRecord::Base.establish_connection(original_connection)
        end
      end

      def test_database_exists_returns_true_for_an_in_memory_db
        assert SQLite3Adapter.database_exists?(database: ":memory:"),
          "Expected in memory database to exist"
      end

      def test_column_types
        owner = Owner.create!(name: "hello".encode("ascii-8bit"))
        owner.reload
        select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ", "
        result = Owner.connection.exec_query <<~SQL
          SELECT #{select}
          FROM   #{Owner.table_name}
          WHERE  #{Owner.primary_key} = #{owner.id}
        SQL

        assert_not(result.rows.first.include?("blob"), "should not store blobs")
      ensure
        owner.delete
      end

      def test_exec_insert
        with_example_table do
          vals = [Relation::QueryAttribute.new("number", 10, Type::Value.new)]
          @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", vals)

          result = @conn.exec_query(
            "select number from ex where number = ?", "SQL", vals)

          assert_equal 1, result.rows.length
          assert_equal 10, result.rows.first.first
        end
      end

      def test_primary_key_returns_nil_for_no_pk
        with_example_table "id int, data string" do
          assert_nil @conn.primary_key("ex")
        end
      end

      def test_connection_no_db
        assert_raises(ArgumentError) do
          Base.sqlite3_connection { }
        end
      end

      def test_bad_timeout
        assert_raises(TypeError) do
          Base.sqlite3_connection database: ":memory:",
                                  adapter: "sqlite3",
                                  timeout: "usa"
        end
      end

      # connection is OK with a nil timeout
      def test_nil_timeout
        conn = Base.sqlite3_connection database: ":memory:",
                                       adapter: "sqlite3",
                                       timeout: nil
        assert conn, "made a connection"
      end

      def test_connect
        assert @conn, "should have connection"
      end

      # sqlite3 defaults to UTF-8 encoding
      def test_encoding
        assert_equal "UTF-8", @conn.encoding
      end

      def test_exec_no_binds
        with_example_table "id int, data string" do
          result = @conn.exec_query("SELECT id, data FROM ex")
          assert_equal 0, result.rows.length
          assert_equal 2, result.columns.length
          assert_equal %w{ id data }, result.columns

          @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
          result = @conn.exec_query("SELECT id, data FROM ex")
          assert_equal 1, result.rows.length
          assert_equal 2, result.columns.length

          assert_equal [[1, "foo"]], result.rows
        end
      end

      def test_exec_query_with_binds
        with_example_table "id int, data string" do
          @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
          result = @conn.exec_query(
            "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)])

          assert_equal 1, result.rows.length
          assert_equal 2, result.columns.length

          assert_equal [[1, "foo"]], result.rows
        end
      end

      def test_exec_query_typecasts_bind_vals
        with_example_table "id int, data string" do
          @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')

          result = @conn.exec_query(
            "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)])

          assert_equal 1, result.rows.length
          assert_equal 2, result.columns.length

          assert_equal [[1, "foo"]], result.rows
        end
      end

      def test_quote_binary_column_escapes_it
        DualEncoding.connection.execute(<<~SQL)
          CREATE TABLE IF NOT EXISTS dual_encodings (
            id integer PRIMARY KEY AUTOINCREMENT,
            name varchar(255),
            data binary
          )
        SQL
        str = (+"\x80").force_encoding("ASCII-8BIT")
        binary = DualEncoding.new name: "いただきます！", data: str
        binary.save!
        assert_equal str, binary.data
      ensure
        DualEncoding.connection.drop_table "dual_encodings", if_exists: true
      end

      def test_type_cast_should_not_mutate_encoding
        name = (+"hello").force_encoding(Encoding::ASCII_8BIT)
        Owner.create(name: name)
        assert_equal Encoding::ASCII_8BIT, name.encoding
      ensure
        Owner.delete_all
      end

      def test_execute
        with_example_table do
          @conn.execute "INSERT INTO ex (number) VALUES (10)"
          records = @conn.execute "SELECT * FROM ex"
          assert_equal 1, records.length

          record = records.first
          assert_equal 10, record["number"]
          assert_equal 1, record["id"]
        end
      end

      def test_quote_string
        assert_equal "''", @conn.quote_string("'")
      end

      def test_insert_logged
        with_example_table do
          sql = "INSERT INTO ex (number) VALUES (10)"
          name = "foo"
          assert_logged [[sql, name, []]] do
            @conn.insert(sql, name)
          end
        end
      end

      def test_insert_id_value_returned
        with_example_table do
          sql = "INSERT INTO ex (number) VALUES (10)"
          idval = "vuvuzela"
          id = @conn.insert(sql, nil, nil, idval)
          assert_equal idval, id
        end
      end

      def test_select_rows
        with_example_table do
          2.times do |i|
            @conn.create "INSERT INTO ex (number) VALUES (#{i})"
          end
          rows = @conn.select_rows "select number, id from ex"
          assert_equal [[0, 1], [1, 2]], rows
        end
      end

      def test_select_rows_logged
        with_example_table do
          sql = "select * from ex"
          name = "foo"
          assert_logged [[sql, name, []]] do
            @conn.select_rows sql, name
          end
        end
      end

      def test_transaction
        with_example_table do
          count_sql = "select count(*) from ex"

          @conn.begin_db_transaction
          @conn.create "INSERT INTO ex (number) VALUES (10)"

          assert_equal 1, @conn.select_rows(count_sql).first.first
          @conn.rollback_db_transaction
          assert_equal 0, @conn.select_rows(count_sql).first.first
        end
      end

      def test_tables
        with_example_table do
          assert_equal %w{ ex }, @conn.tables
          with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer", "people" do
            assert_equal %w{ ex people }.sort, @conn.tables.sort
          end
        end
      end

      def test_tables_logs_name
        sql = <<~SQL
          SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table')
        SQL
        assert_logged [[sql.squish, "SCHEMA", []]] do
          @conn.tables
        end
      end

      def test_table_exists_logs_name
        with_example_table do
          sql = <<~SQL
            SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table')
          SQL
          assert_logged [[sql.squish, "SCHEMA", []]] do
            assert @conn.table_exists?("ex")
          end
        end
      end

      def test_columns
        with_example_table do
          columns = @conn.columns("ex").sort_by(&:name)
          assert_equal 2, columns.length
          assert_equal %w{ id number }.sort, columns.map(&:name)
          assert_equal [nil, nil], columns.map(&:default)
          assert_equal [true, true], columns.map(&:null)
        end
      end

      def test_columns_with_default
        with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer default 10" do
          column = @conn.columns("ex").find { |x|
            x.name == "number"
          }
          assert_equal "10", column.default
        end
      end

      def test_columns_with_not_null
        with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer not null" do
          column = @conn.columns("ex").find { |x| x.name == "number" }
          assert_not column.null, "column should not be null"
        end
      end

      def test_add_column_with_not_null
        with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer not null" do
          assert_nothing_raised { @conn.add_column :ex, :name, :string, null: false }
          column = @conn.columns("ex").find { |x| x.name == "name" }
          assert_not column.null, "column should not be null"
        end
      end

      def test_indexes_logs
        with_example_table do
          assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do
            @conn.indexes("ex")
          end
        end
      end

      def test_no_indexes
        assert_equal [], @conn.indexes("items")
      end

      def test_index
        with_example_table do
          @conn.add_index "ex", "id", unique: true, name: "fun"
          index = @conn.indexes("ex").find { |idx| idx.name == "fun" }

          assert_equal "ex", index.table
          assert index.unique, "index is unique"
          assert_equal ["id"], index.columns
        end
      end

      def test_index_with_if_not_exists
        with_example_table do
          @conn.add_index "ex", "id"

          assert_nothing_raised do
            @conn.add_index "ex", "id", if_not_exists: true
          end
        end
      end

      def test_non_unique_index
        with_example_table do
          @conn.add_index "ex", "id", name: "fun"
          index = @conn.indexes("ex").find { |idx| idx.name == "fun" }
          assert_not index.unique, "index is not unique"
        end
      end

      def test_compound_index
        with_example_table do
          @conn.add_index "ex", %w{ id number }, name: "fun"
          index = @conn.indexes("ex").find { |idx| idx.name == "fun" }
          assert_equal %w{ id number }.sort, index.columns.sort
        end
      end

      if ActiveRecord::Base.connection.supports_expression_index?
        def test_expression_index
          with_example_table do
            @conn.add_index "ex", "max(id, number)", name: "expression"
            index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
            assert_equal "max(id, number)", index.columns
          end
        end

        def test_expression_index_with_where
          with_example_table do
            @conn.add_index "ex", "id % 10, max(id, number)", name: "expression", where: "id > 1000"
            index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
            assert_equal "id % 10, max(id, number)", index.columns
            assert_equal "id > 1000", index.where
          end
        end

        def test_complicated_expression
          with_example_table do
            @conn.execute "CREATE INDEX expression ON ex (id % 10, (CASE WHEN number > 0 THEN max(id, number) END))WHERE(id > 1000)"
            index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
            assert_equal "id % 10, (CASE WHEN number > 0 THEN max(id, number) END)", index.columns
            assert_equal "(id > 1000)", index.where
          end
        end

        def test_not_everything_an_expression
          with_example_table do
            @conn.add_index "ex", "id, max(id, number)", name: "expression"
            index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
            assert_equal "id, max(id, number)", index.columns
          end
        end
      end

      def test_primary_key
        with_example_table do
          assert_equal "id", @conn.primary_key("ex")
          with_example_table "internet integer PRIMARY KEY AUTOINCREMENT, number integer not null", "foos" do
            assert_equal "internet", @conn.primary_key("foos")
          end
        end
      end

      def test_no_primary_key
        with_example_table "number integer not null" do
          assert_nil @conn.primary_key("ex")
        end
      end

      class Barcode < ActiveRecord::Base
        self.primary_key = "code"
      end

      def test_copy_table_with_existing_records_have_custom_primary_key
        connection = Barcode.connection
        connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) do |t|
          t.text :other_attr
        end
        code = "214fe0c2-dd47-46df-b53b-66090b3c1d40"
        Barcode.create!(code: code, other_attr: "xxx")

        connection.remove_column("barcodes", "other_attr")

        assert_equal code, Barcode.first.id
      ensure
        Barcode.reset_column_information
      end

      def test_copy_table_with_composite_primary_keys
        connection = Barcode.connection
        connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t|
          t.string :region
          t.string :code
          t.text :other_attr
        end
        region = "US"
        code = "214fe0c2-dd47-46df-b53b-66090b3c1d40"
        Barcode.create!(region: region, code: code, other_attr: "xxx")

        connection.remove_column("barcodes", "other_attr")

        assert_equal ["region", "code"], connection.primary_keys("barcodes")

        barcode = Barcode.first
        assert_equal region, barcode.region
        assert_equal code, barcode.code
      ensure
        Barcode.reset_column_information
      end

      def test_custom_primary_key_in_create_table
        connection = Barcode.connection
        connection.create_table :barcodes, id: false, force: true do |t|
          t.primary_key :id, :string
        end

        assert_equal "id", connection.primary_key("barcodes")

        custom_pk = Barcode.columns_hash["id"]

        assert_equal :string, custom_pk.type
        assert_not custom_pk.null
      ensure
        Barcode.reset_column_information
      end

      def test_custom_primary_key_in_change_table
        connection = Barcode.connection
        connection.create_table :barcodes, id: false, force: true do |t|
          t.integer :dummy
        end
        connection.change_table :barcodes do |t|
          t.primary_key :id, :string
        end

        assert_equal "id", connection.primary_key("barcodes")

        custom_pk = Barcode.columns_hash["id"]

        assert_equal :string, custom_pk.type
        assert_not custom_pk.null
      ensure
        Barcode.reset_column_information
      end

      def test_add_column_with_custom_primary_key
        connection = Barcode.connection
        connection.create_table :barcodes, id: false, force: true do |t|
          t.integer :dummy
        end
        connection.add_column :barcodes, :id, :string, primary_key: true

        assert_equal "id", connection.primary_key("barcodes")

        custom_pk = Barcode.columns_hash["id"]

        assert_equal :string, custom_pk.type
        assert_not custom_pk.null
      ensure
        Barcode.reset_column_information
      end

      def test_remove_column_preserves_partial_indexes
        connection = Barcode.connection
        connection.create_table :barcodes, force: true do |t|
          t.string :code
          t.string :region
          t.boolean :bool_attr

          t.index :code, unique: true, where: :bool_attr, name: "partial"
        end
        connection.remove_column :barcodes, :region

        index = connection.indexes("barcodes").find { |idx| idx.name == "partial" }
        assert_equal "bool_attr", index.where
      ensure
        Barcode.reset_column_information
      end

      def test_supports_extensions
        assert_not @conn.supports_extensions?, "does not support extensions"
      end

      def test_respond_to_enable_extension
        assert_respond_to @conn, :enable_extension
      end

      def test_respond_to_disable_extension
        assert_respond_to @conn, :disable_extension
      end

      def test_statement_closed
        db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
        db = ::SQLite3::Database.new(db_config.database)

        statement = ::SQLite3::Statement.new(db,
                                           "CREATE TABLE statement_test (number integer not null)")
        statement.stub(:step, -> { raise ::SQLite3::BusyException.new("busy") }) do
          assert_called(statement, :columns, returns: []) do
            assert_called(statement, :close) do
              ::SQLite3::Statement.stub(:new, statement) do
                assert_raises ActiveRecord::StatementInvalid do
                  @conn.exec_query "select * from statement_test"
                end
              end
            end
          end
        end
      end

      def test_db_is_not_readonly_when_readonly_option_is_false
        conn = Base.sqlite3_connection database: ":memory:",
                                       adapter: "sqlite3",
                                       readonly: false

        assert_not_predicate conn.raw_connection, :readonly?
      end

      def test_db_is_not_readonly_when_readonly_option_is_unspecified
        conn = Base.sqlite3_connection database: ":memory:",
                                       adapter: "sqlite3"

        assert_not_predicate conn.raw_connection, :readonly?
      end

      def test_db_is_readonly_when_readonly_option_is_true
        conn = Base.sqlite3_connection database: ":memory:",
                                       adapter: "sqlite3",
                                       readonly: true

        assert_predicate conn.raw_connection, :readonly?
      end

      def test_writes_are_not_permitted_to_readonly_databases
        conn = Base.sqlite3_connection database: ":memory:",
                                       adapter: "sqlite3",
                                       readonly: true

        assert_raises(ActiveRecord::StatementInvalid, /SQLite3::ReadOnlyException/) do
          conn.execute("CREATE TABLE test(id integer)")
        end
      end

      private
        def assert_logged(logs)
          subscriber = SQLSubscriber.new
          subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
          yield
          assert_equal logs, subscriber.logged
        ensure
          ActiveSupport::Notifications.unsubscribe(subscription)
        end

        def with_example_table(definition = nil, table_name = "ex", &block)
          definition ||= <<~SQL
            id integer PRIMARY KEY AUTOINCREMENT,
            number integer
          SQL
          super(@conn, table_name, definition, &block)
        end
    end
  end
end
