# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ExceedQueryLimitHelpers do
  before do
    stub_const('TestQueries', Class.new(ActiveRecord::Base))
    stub_const('TestMatcher', Class.new)

    TestQueries.class_eval do
      self.table_name = 'schema_migrations'

      def self._returning_columns_for_insert(...)
        ['version']
      end
    end

    TestMatcher.class_eval do
      include ExceedQueryLimitHelpers

      def expected
        ActiveRecord::QueryRecorder.new do
          2.times { TestQueries.count }
        end
      end
    end
  end

  describe '#diff_query_group_message' do
    it 'prints a group helpfully' do
      test_matcher = TestMatcher.new
      suffixes = {
        'WHERE x = z' => [1, 1],
        'WHERE x = y' => [1, 2],
        'LIMIT 1' => [1, 0]
      }

      message = test_matcher.diff_query_group_message('SELECT * FROM foo', suffixes)

      expect(message).to eq(<<~MSG.chomp)
      SELECT * FROM foo...
      -- (expected: 1, got: 1)
         WHERE x = z
      -- (expected: 1, got: 2)
         WHERE x = y
      -- (expected: 1, got: 0)
         LIMIT 1
      MSG
    end
  end

  describe '#diff_query_counts' do
    let(:expected) do
      ActiveRecord::QueryRecorder.new do
        TestQueries.where(version: 'foobar').to_a
        TestQueries.where(version: 'also foobar and baz').to_a
        TestQueries.count
        TestQueries.first
        TestQueries.where(version: 'foobar').to_a
        TestQueries.where(version: 'x').update_all(version: 'y')
        TestQueries.where(version: 'foobar').count
        TestQueries.where(version: 'z').delete_all
        Project.where(id: 1).pluck(:title)
      end
    end

    let(:actual) do
      ActiveRecord::QueryRecorder.new do
        TestQueries.where(version: 'foobar').to_a
        TestQueries.where(version: 'also foobar and baz').to_a
        TestQueries.count
        TestQueries.create!(version: 'x')
        TestQueries.where(version: 'foobar').to_a
        TestQueries.where(version: 'x').update_all(version: 'y')
        TestQueries.where(version: 'foobar').count
        TestQueries.count
        TestQueries.where(version: 'y').update_all(version: 'z')
        TestQueries.where(version: 'z').delete_all
        Project.where(id: 2).pluck(:title)
      end
    end

    it 'merges two query counts, showing only diffs' do
      test_matcher = TestMatcher.new

      diff = test_matcher.diff_query_counts(
        test_matcher.count_queries(expected),
        test_matcher.count_queries(actual)
      )

      expect(diff).to eq({
        "SELECT \"schema_migrations\".* FROM \"schema_migrations\"" => {
          "ORDER BY \"schema_migrations\".\"version\" ASC LIMIT 1" => [1, 0]
        },
        "SELECT COUNT(*) FROM \"schema_migrations\"" => { "" => [1, 2] },
        "UPDATE \"schema_migrations\"" => {
          "SET \"version\" = 'z' WHERE \"schema_migrations\".\"version\" = 'y'" => [0, 1]
        },
        "SAVEPOINT active_record_1" => { "" => [0, 1] },
        "INSERT INTO \"schema_migrations\" (\"version\")" => {
          "VALUES ('x') RETURNING \"version\"" => [0, 1]
        },
        "RELEASE SAVEPOINT active_record_1" => { "" => [0, 1] }
      })
    end

    it 'can show common queries if so desired' do
      test_matcher = TestMatcher.new.show_common_queries

      diff = test_matcher.diff_query_counts(
        test_matcher.count_queries(expected),
        test_matcher.count_queries(actual)
      )

      expect(diff).to eq({
        "SELECT \"schema_migrations\".* FROM \"schema_migrations\"" => {
          "WHERE \"schema_migrations\".\"version\" = 'foobar'" => [2, 2],
          "WHERE \"schema_migrations\".\"version\" = 'also foobar and baz'" => [1, 1],
          "ORDER BY \"schema_migrations\".\"version\" ASC LIMIT 1" => [1, 0]
        },
        "SELECT COUNT(*) FROM \"schema_migrations\"" => {
          "" => [1, 2],
          "WHERE \"schema_migrations\".\"version\" = 'foobar'" => [1, 1]
        },
        "UPDATE \"schema_migrations\"" => {
          "SET \"version\" = 'y' WHERE \"schema_migrations\".\"version\" = 'x'" => [1, 1],
          "SET \"version\" = 'z' WHERE \"schema_migrations\".\"version\" = 'y'" => [0, 1]
        },
        "DELETE FROM \"schema_migrations\"" => {
          "WHERE \"schema_migrations\".\"version\" = 'z'" => [1, 1]
        },
        "SAVEPOINT active_record_1" => {
          "" => [0, 1]
        },
        "INSERT INTO \"schema_migrations\" (\"version\")" => {
          "VALUES ('x') RETURNING \"version\"" => [0, 1]
        },
        "RELEASE SAVEPOINT active_record_1" => {
          "" => [0, 1]
        },
        "SELECT \"projects\".\"name\" FROM \"projects\"" => {
          "WHERE \"projects\".\"id\" = 1" => [1, 0],
          "WHERE \"projects\".\"id\" = 2" => [0, 1]
        }
      })
    end
  end

  describe '#count_queries' do
    it 'handles queries with suffixes over multiple lines' do
      test_matcher = TestMatcher.new

      recorder = ActiveRecord::QueryRecorder.new do
        TestQueries.find_by(version: %w[foo bar baz].join("\n"))
        TestQueries.find_by(version: %w[foo biz baz].join("\n"))
        TestQueries.find_by(version: %w[foo bar baz].join("\n"))
      end

      recorder.count

      expect(test_matcher.count_queries(recorder)).to eq({
        'SELECT "schema_migrations".* FROM "schema_migrations"' => {
          %(WHERE "schema_migrations"."version" = 'foo\nbar\nbaz' LIMIT 1) => 2,
          %(WHERE "schema_migrations"."version" = 'foo\nbiz\nbaz' LIMIT 1) => 1
        }
      })
    end

    it 'can aggregate queries' do
      test_matcher = TestMatcher.new

      recorder = ActiveRecord::QueryRecorder.new do
        TestQueries.where(version: 'foobar').to_a
        TestQueries.where(version: 'also foobar and baz').to_a
        TestQueries.count
        TestQueries.create!(version: 'x')
        TestQueries.first
        TestQueries.where(version: 'foobar').to_a
        TestQueries.where(version: 'x').update_all(version: 'y')
        TestQueries.where(version: 'foobar').count
        TestQueries.count
        TestQueries.where(version: 'y').update_all(version: 'z')
        TestQueries.where(version: 'z').delete_all
      end

      recorder.count

      expect(test_matcher.count_queries(recorder)).to eq({
        'SELECT "schema_migrations".* FROM "schema_migrations"' => {
          %q(WHERE "schema_migrations"."version" = 'foobar') => 2,
          %q(WHERE "schema_migrations"."version" = 'also foobar and baz') => 1,
          %q(ORDER BY "schema_migrations"."version" ASC LIMIT 1) => 1
        },
        'SELECT COUNT(*) FROM "schema_migrations"' => {
          "" => 2,
          %q(WHERE "schema_migrations"."version" = 'foobar') => 1
        },
        'SAVEPOINT active_record_1' => { "" => 1 },
        'INSERT INTO "schema_migrations" ("version")' => {
          %q[VALUES ('x') RETURNING "version"] => 1
        },
        'RELEASE SAVEPOINT active_record_1' => { "" => 1 },
        'UPDATE "schema_migrations"' => {
          %q(SET "version" = 'y' WHERE "schema_migrations"."version" = 'x') => 1,
          %q(SET "version" = 'z' WHERE "schema_migrations"."version" = 'y') => 1
        },
        'DELETE FROM "schema_migrations"' => {
          %q(WHERE "schema_migrations"."version" = 'z') => 1
        }
      })
    end
  end

  it 'can count queries' do
    test_matcher = TestMatcher.new
    test_matcher.verify_count do
      TestQueries.where(version: 'foobar').to_a
      TestQueries.where(version: 'also foobar and baz').to_a
      TestQueries.first
      TestQueries.count
    end

    expect(test_matcher.actual_count).to eq(4)
  end

  it 'can select specific queries' do
    test_matcher = TestMatcher.new.for_query(/foobar/)
    test_matcher.verify_count do
      TestQueries.where(version: 'foobar').to_a
      TestQueries.where(version: 'also foobar and baz').to_a
      TestQueries.first
      TestQueries.count
    end

    expect(test_matcher.actual_count).to eq(2)
  end

  it 'can filter specific models' do
    test_matcher = TestMatcher.new.for_model(TestQueries)
    test_matcher.verify_count do
      TestQueries.first
      TestQueries.connection.execute('select 1')
    end

    expect(test_matcher.actual_count).to eq(1)
  end

  it 'can ignore specific queries' do
    test_matcher = TestMatcher.new.ignoring(/foobar/)
    test_matcher.verify_count do
      TestQueries.where(version: 'foobar').to_a
      TestQueries.where(version: 'also foobar and baz').to_a
      TestQueries.first
    end

    expect(test_matcher.actual_count).to eq(1)
  end

  it 'can perform inclusion and exclusion' do
    test_matcher = TestMatcher.new.for_query(/foobar/).ignoring(/baz/)
    test_matcher.verify_count do
      TestQueries.where(version: 'foobar').to_a
      TestQueries.where(version: 'also foobar and baz').to_a
      TestQueries.first
      TestQueries.count
    end

    expect(test_matcher.actual_count).to eq(1)
  end

  it 'does not contain marginalia annotations' do
    test_matcher = TestMatcher.new
    test_matcher.verify_count do
      2.times { TestQueries.count }
      TestQueries.first
    end

    aggregate_failures do
      expect(test_matcher.log_message)
        .to match(%r{ORDER BY.*#{TestQueries.table_name}.*LIMIT 1})
      expect(test_matcher.log_message)
        .not_to match(%r{/\*.*correlation_id.*\*/})
    end
  end
end
