# frozen_string_literal: true

module RSpec
  module Terraform
    module Matchers
      # rubocop:disable Metrics/ClassLength
      class IncludeOutputChange
        attr_reader :definition, :value, :plan

        def initialize(definition = {})
          @definition = definition
          @value = nil
        end

        def matches?(plan)
          @plan = plan

          !value_matches(plan).empty?
        end

        def with_value(value)
          @value = maybe_box_value(value)
          self
        end

        def failure_message
          "\nexpected: #{positive_expected_line}" \
            "\n     got: #{positive_got_line}"
        end

        private

        def definition_matches(plan)
          plan.output_changes_matching(definition)
        end

        def value_matches(plan)
          matches = definition_matches(plan)
          return matches unless value

          expected = value_matcher(value)

          matches.filter do |output_change|
            change = output_change.change
            after = change.after_object
            actual = resolved_value(value, after)

            expected&.matches?(actual)
          end
        end

        def maybe_box_value(value)
          if value.respond_to?(:matches?)
            value
          else
            RubyTerraform::Models::Objects.box(value)
          end
        end

        def value_matcher(expected)
          return expected if expected.respond_to?(:matches?)

          RSpec::Matchers::BuiltIn::Eq.new(expected)
        end

        def resolved_value(expected, actual)
          return actual&.unbox if expected.respond_to?(:matches?)

          actual
        end

        def positive_expected_line
          maybe_with_expected_value(
            maybe_with_definition(
              positive_plan_line
            )
          )
        end

        def positive_plan_line
          'a plan including at least one output change'
        end

        def maybe_with_definition(expected_line)
          unless definition.empty?
            expected_line =
              "#{expected_line} matching definition:\n#{definition_lines}"
          end
          expected_line
        end

        def maybe_with_expected_value(expected_line)
          unless value.nil?
            expected_line =
              "#{expected_line}\n          with value after " \
              "the output change is applied of:\n#{expected_value_lines}"
          end
          expected_line
        end

        def definition_lines
          indent = '            '
          definition
            .collect { |k, v| "#{indent}#{k} = #{v.inspect}" }
            .join("\n")
        end

        def expected_value_lines
          renderable_value = with_matcher_renderable(value)
          value_object =
            RubyTerraform::Models::Values.map(value: renderable_value)
          value_object.render(level: 6, bare: true)
        end

        def positive_got_line
          if plan.output_changes.empty?
            'a plan including no output changes.'
          else
            with_available_output_changes(
              maybe_with_relevant_output_changes(
                'a plan including no matching output changes.'
              )
            )
          end
        end

        def with_available_output_changes(got_line)
          "#{got_line}\n          available output changes are:" \
            "\n#{available_output_change_lines}"
        end

        def maybe_with_relevant_output_changes(got_line)
          unless value.nil?
            got_line =
              "#{got_line}\n          relevant output changes are:" \
              "\n#{relevant_output_change_lines}"
          end
          got_line
        end

        def available_output_change_lines
          available_lines = plan.output_changes.collect do |oc|
            name = oc.name
            actions = oc.change.actions.join(', ')
            "            - #{name} (#{actions})"
          end
          available_lines.join("\n")
        end

        def relevant_output_change_lines
          relevant_lines = definition_matches(plan).collect do |oc|
            name = oc.name
            actions = oc.change.actions.join(', ')
            value = oc.change.after_object
            value_object =
              RubyTerraform::Models::Values.map(value: value)
            value_lines = value_object.render(level: 8, bare: true)

            "            - #{name} (#{actions})\n#{value_lines}"
          end
          relevant_lines.join("\n")
        end

        def with_matcher_renderable(value)
          return value if value.respond_to?(:render)

          value.define_singleton_method(:render) do |_|
            "a value satisfying: #{value.description}"
          end

          value
        end
      end
      # rubocop:enable Metrics/ClassLength
    end
  end
end