require "pact_broker/logging"
require "pact_broker/matrix/reason"
require "forwardable"

module PactBroker
  module Matrix
    class DeploymentStatusSummary
      include PactBroker::Logging
      extend Forwardable

      attr_reader :query_results, :all_rows
      delegate [:considered_rows, :ignored_rows, :resolved_selectors, :resolved_ignore_selectors, :integrations] => :query_results

      def initialize(query_results)
        @query_results = query_results
        @all_rows = query_results.rows
        @dummy_selectors = create_dummy_selectors
      end

      def counts
        {
          success: considered_rows.count(&:success),
          failed: considered_rows.count { |row| row.success == false },
          unknown: required_integrations_without_a_row.count + considered_rows.count { |row| row.success.nil? },
          ignored: resolved_ignore_selectors.any? ? ignored_rows.count : nil
        }.compact
      end

      def deployable?
        return false if considered_specified_selectors_that_do_not_exist.any?
        return nil if considered_rows.any?{ |row| row.success.nil? }
        return nil if required_integrations_without_a_row.any?
        considered_rows.all?(&:success) # true if considered_rows is empty
      end

      def reasons
        error_messages.any? ? warning_messages + error_messages  : warning_messages + success_messages
      end

      private

      attr_reader :dummy_selectors

      def error_messages
        @error_messages ||= begin
          messages = []
          messages.concat(specified_selectors_do_not_exist_messages)
          if messages.empty?
            messages.concat(missing_reasons)
            messages.concat(failure_messages)
            messages.concat(not_ever_verified_reasons)
          end
          messages.uniq
        end
      end

      def warning_messages
        resolved_ignore_selectors.select(&:pacticipant_or_version_does_not_exist?).collect { | s | IgnoreSelectorDoesNotExist.new(s) } +
          bad_practice_warnings
      end

      def bad_practice_warnings
        warnings = []

        if no_to_tag_or_environment_specified?
          warnings << NoEnvironmentSpecified.new
        end

        if selector_without_pacticipant_version_number_specified?
          warnings << SelectorWithoutPacticipantVersionNumberSpecified.new
        end

        warnings
      end

      def selector_without_pacticipant_version_number_specified?
        # If only the pacticipant name is specified, it can't be a can-i-deploy query, must be a matrix UI query
        resolved_selectors
          .select(&:specified?)
          .reject(&:only_pacticipant_name_specified?)
          .reject(&:pacticipant_version_specified_in_original_selector?)
          .any?
      end


      def more_than_one_selector_specified?
        # If only the pacticipant name is specified, it can't be a can-i-deploy query, must be a matrix UI query
        resolved_selectors
          .select(&:specified?)
          .reject(&:only_pacticipant_name_specified?)
          .any?
      end

      def no_to_tag_or_environment_specified?
        !(query_results.options[:tag] || query_results.options[:environment_name])
      end

      def considered_specified_selectors_that_do_not_exist
        resolved_selectors.select(&:consider?).select(&:specified_version_that_does_not_exist?)
      end

      def specified_selectors_that_do_not_exist
        resolved_selectors.select(&:specified_version_that_does_not_exist?)
      end

      def specified_selectors_do_not_exist_messages
        specified_selectors_that_do_not_exist.select(&:consider?).collect { | selector | SpecifiedVersionDoesNotExist.new(selector) }
      end

      def not_ever_verified_reasons
        considered_rows.select{ | row | row.success.nil? }.collect{ |row | pact_not_ever_verified_by_provider(row) }
      end

      def failure_messages
        considered_rows.select{ |row| row.success == false }.collect { | row | VerificationFailed.new(*selectors_for(row)) }
      end

      def success_messages
        if considered_rows.all?(&:success) && required_integrations_without_a_row.empty?
          if considered_rows.any?
            [Successful.new]
          else
            [NoDependenciesMissing.new]
          end
        else
          []
        end.flatten.uniq
      end

      # For deployment, the consumer requires the provider,
      # but the provider does not require the consumer
      # This method tells us which providers are missing.
      # Technically, it tells us which integrations do not have a row
      # because the pact that belongs to the consumer version has
      # in fact been verified, but not by the provider version specified
      # in the query (because left outer join)
      #
      # Imagine query for deploying Foo v3 to prod with the following matrix:
      # Foo v2 -> Bar v1 (latest prod) [this line not included because CV doesn't match]
      # Foo v3 -> Bar v2               [this line not included because PV doesn't match]
      #
      # No matrix considered_rows would be returned. This method identifies that we have no row for
      # the Foo -> Bar integration, and therefore cannot deploy Foo.
      # However, if we were to try and deploy the provider, Bar, that would be ok
      # as Bar does not rely on Foo, so this method would not return that integration.
      # UPDATE:
      # The matrix query now returns a row with blank provider version/verification fields
      # so the above comment is now redundant.
      # I'm not sure if this piece of code can ever return a list with anything in it any more.
      # Will log it for a while and see.
      def required_integrations_without_a_row
        @required_integrations_without_a_row ||= begin
          integrations.select(&:required?).select do | integration |
            !row_exists_for_integration(integration)
          end
        end.tap { |it| log_required_integrations_without_a_row_occurred(it) if it.any? }
      end

      def log_required_integrations_without_a_row_occurred integrations
        logger.info("required_integrations_without_a_row returned non empty", payload: { integrations: integrations, considered_rows: considered_rows })
      end

      def row_exists_for_integration(integration)
        all_rows.find { | row | integration.matches_pacticipant_ids?(row) }
      end

      def missing_reasons
        required_integrations_without_a_row.collect do | integration |
          pact_not_verified_by_required_provider_version(integration)
        end.flatten
      end

      def selectors_without_a_version_for(integration)
        selectors_with_non_existing_versions.select do | selector |
          integration.involves_pacticipant_with_name?(selector.pacticipant_name)
        end
      end

      def selectors_with_non_existing_versions
        @selectors_with_non_existing_versions ||= resolved_selectors.select(&:latest_tagged_version_that_does_not_exist?)
      end

      def missing_specified_version_reasons(selectors)
        selectors.collect(&:version_does_not_exist_description)
      end

      def pact_not_verified_by_required_provider_version(integration)
        PactNotVerifiedByRequiredProviderVersion.new(*selectors_for(integration))
      end

      def pact_not_ever_verified_by_provider(row)
        PactNotEverVerifiedByProvider.new(*selectors_for(row))
      end

      def selector_for(pacticipant_name)
        resolved_selectors.find{ | s| s.pacticipant_name == pacticipant_name } ||
          dummy_selectors.find{ | s| s.pacticipant_name == pacticipant_name }
      end

      def selectors_for(row)
        [selector_for(row.consumer_name), selector_for(row.provider_name)]
      end

      # When the user has not specified a version of the provider (eg no 'latest' and/or 'tag', which means 'all versions')
      # so the "add inferred selectors" code in the Matrix::Repository has not run,
      # we may end up with considered_rows for which we do not have a selector.
      # To solve this, create dummy selectors from the row and integration data.
      def create_dummy_selectors
        (dummy_selectors_from_considered_rows + dummy_selectors_from_integrations).uniq
      end

      def dummy_selectors_from_integrations
        integrations.collect do | row |
          dummy_consumer_selector = ResolvedSelector.for_pacticipant(row.consumer, {}, :inferred, false)
          dummy_provider_selector = ResolvedSelector.for_pacticipant(row.provider, {}, :inferred, false)
          [dummy_consumer_selector, dummy_provider_selector]
        end.flatten
      end

      def dummy_selectors_from_considered_rows
        considered_rows.collect do | row |
          dummy_consumer_selector = ResolvedSelector.for_pacticipant_and_version(row.consumer, row.consumer_version, {}, :inferred, false)
          dummy_provider_selector = row.provider_version ?
            ResolvedSelector.for_pacticipant_and_version(row.provider, row.provider_version, {}, :inferred, false) :
            ResolvedSelector.for_pacticipant(row.provider, {}, :inferred, false)
          [dummy_consumer_selector, dummy_provider_selector]
        end.flatten
      end

      # experimental
      def warnings_for_missing_interactions
        considered_rows.select(&:success).collect do | row |
          begin
            if row.verification.interactions_missing_test_results.any? && !row.verification.all_interactions_missing_test_results?
              InteractionsMissingVerifications.new(selector_for(row.consumer_name), selector_for(row.provider_name), row.verification.interactions_missing_test_results)
            end
          rescue StandardError => e
            logger.warn("Error determining if there were missing interaction verifications", e)
            nil
          end
        end.compact.tap { |it| report_missing_interaction_verifications(it) if it.any? }
      end

      def report_missing_interaction_verifications(messages)
        logger.warn("Interactions missing verifications", messages)
      end
    end
  end
end