# frozen_string_literal: true

require 'forwardable'
require 'capybara/dsl'
require 'active_support/core_ext/array/extract_options'

module QA
  module Resource
    class Base
      extend SingleForwardable
      include ApiFabricator
      extend Capybara::DSL

      NoValueError = Class.new(RuntimeError)

      def_delegators :evaluator, :attribute

      def self.fabricate!(*args, &prepare_block)
        fabricate_via_api!(*args, &prepare_block)
      rescue NotImplementedError
        fabricate_via_browser_ui!(*args, &prepare_block)
      end

      def self.fabricate_via_browser_ui!(*args, &prepare_block)
        options = args.extract_options!
        resource = options.fetch(:resource) { new }
        parents = options.fetch(:parents) { [] }

        do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
          log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }

          current_url
        end
      end

      def self.fabricate_via_api!(*args, &prepare_block)
        options = args.extract_options!
        resource = options.fetch(:resource) { new }
        parents = options.fetch(:parents) { [] }

        raise NotImplementedError unless resource.api_support?

        resource.eager_load_api_client!

        do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
          log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
        end
      end

      def fabricate!(*_args)
        raise NotImplementedError
      end

      def visit!
        visit(web_url)
      end

      def populate(*attributes)
        attributes.each(&method(:public_send))
      end

      def wait(max: 60, interval: 0.1)
        QA::Support::Waiter.wait(max: max, interval: interval)
      end

      private

      def populate_attribute(name, block)
        value = attribute_value(name, block)

        raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value

        value
      end

      def attribute_value(name, block)
        api_value = api_resource&.dig(name)

        if api_value && block
          log_having_both_api_result_and_block(name, api_value)
        end

        api_value || (block && instance_exec(&block))
      end

      def log_having_both_api_result_and_block(name, api_value)
        QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
      end

      def self.do_fabricate!(resource:, prepare_block:, parents: [])
        prepare_block.call(resource) if prepare_block

        resource_web_url = yield
        resource.web_url = resource_web_url

        resource
      end
      private_class_method :do_fabricate!

      def self.log_fabrication(method, resource, parents, args)
        return yield unless Runtime::Env.debug?

        start = Time.now
        prefix = "==#{'=' * parents.size}>"
        msg = [prefix]
        msg << "Built a #{name}"
        msg << "as a dependency of #{parents.last}" if parents.any?
        msg << "via #{method}"

        yield.tap do
          msg << "in #{Time.now - start} seconds"
          puts msg.join(' ')
          puts if parents.empty?
        end
      end
      private_class_method :log_fabrication

      def self.evaluator
        @evaluator ||= Base::DSL.new(self)
      end
      private_class_method :evaluator

      class DSL
        def initialize(base)
          @base = base
        end

        def attribute(name, &block)
          @base.module_eval do
            attr_writer(name)

            define_method(name) do
              instance_variable_get("@#{name}") ||
                instance_variable_set(
                  "@#{name}",
                  populate_attribute(name, block))
            end
          end
        end
      end

      attribute :web_url
    end
  end
end
