# frozen_string_literal: true

require_relative 'base'

module ChronoTest
  module Matchers
    module Table
      class Base < ChronoTest::Matchers::Base
        protected

        # Database statements
        #
        def relation_exists?(options)
          schema = options[:in]
          kind   = options[:kind] == :view ? 'v' : 'r'
          select_value(<<~SQL.squish, [table, schema], 'Check table exists') == true
            SELECT EXISTS (
              SELECT 1
                FROM pg_class c
                LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
               WHERE c.relkind = '#{kind}'
                 AND c.relname = ?
                 AND n.nspname = ?
            )
          SQL
        end
      end

      # ##################################################################
      # Checks that a table exists in the Public schema
      #
      class HavePublicBacking < Base
        def matches?(table)
          super

          relation_exists? in: public_schema
        end

        def description
          'be in the public schema'
        end

        def failure_message
          "expected #{table} to exist in the #{public_schema} schema"
        end

        def failure_message_when_negated
          "expected #{table} to not exist in the #{public_schema} schema"
        end
      end

      def have_public_backing
        HavePublicBacking.new
      end

      # ##################################################################
      # Checks that a table exists in the Temporal schema
      #
      class HaveTemporalBacking < Base
        def matches?(table)
          super

          relation_exists? in: temporal_schema
        end

        def description
          'be in the temporal schema'
        end

        def failure_message
          "expected #{table} to exist in the #{temporal_schema} schema"
        end

        def failure_message_when_negated
          "expected #{table} to not exist in the #{temporal_schema} schema"
        end
      end

      def have_temporal_backing
        HaveTemporalBacking.new
      end

      # ##################################################################
      # Checks that a table exists in the History schema and inherits from
      # the one in the Temporal schema
      #
      class HaveHistoryBacking < Base
        def matches?(table)
          super

          table_exists? &&
            inherits_from_temporal? &&
            has_consistency_constraint? &&
            has_history_indexes?
        end

        def description
          'be in history schema'
        end

        def failure_message
          "expected #{table} ".tap do |message|
            message << [
              ("to exist in the #{history_schema} schema"    unless @existence),
              ("to inherit from #{temporal_schema}.#{table}" unless @inheritance),
              ('to have a timeline consistency constraint'   unless @constraint),
              ('to have history indexes'                     unless @indexes)
            ].compact.to_sentence
          end
        end

        def failure_message_when_negated
          "expected #{table} ".tap do |message|
            message << [
              ("to not exist in the #{history_schema} schema"    if @existence),
              ("to not inherit from #{temporal_schema}.#{table}" if @inheritance),
              ('to not have a timeline consistency constraint'   if @constraint),
              ('to not have history indexes'                     if @indexes)
            ].compact.to_sentence
          end
        end

        private

        def table_exists?
          @existence = relation_exists? in: history_schema
        end

        def inherits_from_temporal?
          binds = ["#{history_schema}.#{table}", "#{temporal_schema}.#{table}"]

          @inheritance = select_value(<<~SQL.squish, binds, 'Check inheritance') == true
            SELECT EXISTS (
              SELECT 1 FROM pg_catalog.pg_inherits
               WHERE inhrelid  = ?::regclass::oid
                 AND inhparent = ?::regclass::oid
            )
          SQL
        end

        def has_history_indexes?
          binds = [history_schema, table]

          indexes = select_values(<<~SQL.squish, binds, 'Check history indexes')
            SELECT indexdef FROM pg_indexes
             WHERE schemaname = ?
               AND tablename  = ?
          SQL

          fqtn = "#{history_schema}.#{table}"

          expected = [
            "CREATE INDEX index_#{table}_temporal_on_lower_validity ON #{fqtn} USING btree (lower(validity))",
            "CREATE INDEX index_#{table}_temporal_on_upper_validity ON #{fqtn} USING btree (upper(validity))",
            "CREATE INDEX index_#{table}_temporal_on_validity ON #{fqtn} USING gist (validity)",

            "CREATE INDEX #{table}_inherit_pkey ON #{fqtn} USING btree (id)",
            "CREATE INDEX #{table}_instance_history ON #{fqtn} USING btree (id, recorded_at)",
            "CREATE UNIQUE INDEX #{table}_pkey ON #{fqtn} USING btree (hid)",
            "CREATE INDEX #{table}_recorded_at ON #{fqtn} USING btree (recorded_at)",
            "CREATE INDEX #{table}_timeline_consistency ON #{fqtn} USING gist (id, validity)"
          ]

          @indexes = (expected - indexes).empty?
        end

        def has_consistency_constraint?
          binds = {
            conname: connection.timeline_consistency_constraint_name(table),
            connamespace: history_schema,
            conrelid: "#{history_schema}.#{table}",
            attname: connection.primary_key(table)
          }

          @constraint = select_value(<<~SQL.squish, binds, 'Check Consistency Constraint') == true
            SELECT EXISTS (
              SELECT 1 FROM pg_catalog.pg_constraint
              WHERE conname = :conname
                AND contype = 'x'
                AND conrelid = :conrelid::regclass
                AND connamespace = (
                  SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = :connamespace
                )
                AND conkey = (
                  SELECT array_agg(attnum) FROM pg_catalog.pg_attribute
                  WHERE attname IN (:attname, 'validity')
                    AND attrelid = :conrelid::regclass
                )
            )
          SQL
        end
      end

      def have_history_backing
        HaveHistoryBacking.new
      end

      # ##################################################################
      # Checks that a table exists in the Public schema, is an updatable
      # view and has an INSERT, UPDATE and DELETE triggers.
      #
      class HavePublicInterface < Base
        def matches?(table)
          super

          view_exists? && [is_updatable?, has_triggers?].all?
        end

        def description
          'be an updatable view'
        end

        def failure_message
          "expected #{table} ".tap do |message|
            message << [
              ("to exist in the #{public_schema} schema" unless @existence),
              ('to be an updatable view'                 unless @updatable),
              ('to have an INSERT trigger'               unless @insert_trigger),
              ('to have an UPDATE trigger'               unless @update_trigger),
              ('to have a DELETE trigger'                unless @delete_trigger)
            ].compact.to_sentence
          end
        end

        def failure_message_when_negated
          "expected #{table} ".tap do |message|
            message << [
              ("to not exist in the #{public_schema} schema" if @existence),
              ('to not be an updatable view'                 if @updatable),
              ('to not have an INSERT trigger'               if @insert_trigger),
              ('to not have an UPDATE trigger'               if @update_trigger),
              ('to not have a DELETE trigger'                if @delete_trigger)
            ].compact.to_sentence
          end
        end

        private

        def view_exists?
          @existence = relation_exists? in: public_schema, kind: :view
        end

        def is_updatable?
          binds = [public_schema, table]

          @updatable = select_value(<<~SQL.squish, binds, 'Check updatable') == 'YES'
            SELECT is_updatable FROM information_schema.views
             WHERE table_schema = ? AND table_name = ?
          SQL
        end

        def has_triggers?
          triggers = select_values(<<~SQL.squish, [public_schema, table], 'Check triggers')
            SELECT t.tgname
              FROM pg_catalog.pg_trigger t, pg_catalog.pg_class c, pg_catalog.pg_namespace n
             WHERE n.oid = c.relnamespace
               AND n.nspname = ?
               AND c.relname = ?;
          SQL

          @insert_trigger = triggers.include? 'chronomodel_insert'
          @update_trigger = triggers.include? 'chronomodel_update'
          @delete_trigger = triggers.include? 'chronomodel_delete'

          @insert_trigger && @update_trigger && @delete_trigger
        end
      end

      def have_public_interface
        HavePublicInterface.new
      end
    end
  end
end
