# frozen_string_literal: true

module ActiveModelSerializers
  module Test
    module Schema
      # A Minitest Assertion that test the response is valid against a schema.
      # @param schema_path [String] a custom schema path
      # @param message [String] a custom error message
      # @return [Boolean] true when the response is valid
      # @return [Minitest::Assertion] when the response is invalid
      # @example
      #   get :index
      #   assert_response_schema
      def assert_response_schema(schema_path = nil, message = nil)
        matcher = AssertResponseSchema.new(schema_path, request, response, message)
        assert(matcher.call, matcher.message)
      end

      def assert_request_schema(schema_path = nil, message = nil)
        matcher = AssertRequestSchema.new(schema_path, request, response, message)
        assert(matcher.call, matcher.message)
      end

      # May be renamed
      def assert_request_response_schema(schema_path = nil, message = nil)
        assert_request_schema(schema_path, message)
        assert_response_schema(schema_path, message)
      end

      def assert_schema(payload, schema_path = nil, message = nil)
        matcher = AssertSchema.new(schema_path, request, response, message, payload)
        assert(matcher.call, matcher.message)
      end

      MissingSchema = Class.new(Minitest::Assertion)
      InvalidSchemaError = Class.new(Minitest::Assertion)

      class AssertSchema
        attr_reader :schema_path, :request, :response, :message, :payload

        # Interface may change.
        def initialize(schema_path, request, response, message, payload = nil)
          require_json_schema!
          @request = request
          @response = response
          @payload = payload
          @schema_path = schema_path || schema_path_default
          @message = message
          @document_store = JsonSchema::DocumentStore.new
          add_schema_to_document_store
        end

        def call
          json_schema.expand_references!(store: document_store)
          status, errors = json_schema.validate(response_body)
          @message = [message, errors.map(&:to_s).to_sentence].compact.join(': ')
          status
        end

        protected

        attr_reader :document_store

        def controller_path
          request.filtered_parameters.with_indifferent_access[:controller]
        end

        def action
          request.filtered_parameters.with_indifferent_access[:action]
        end

        def schema_directory
          ActiveModelSerializers.config.schema_path
        end

        def schema_full_path
          "#{schema_directory}/#{schema_path}"
        end

        def schema_path_default
          "#{controller_path}/#{action}.json"
        end

        def schema_data
          load_json_file(schema_full_path)
        end

        def response_body
          load_json(response.body)
        end

        def request_params
          request.env['action_dispatch.request.request_parameters']
        end

        def json_schema
          @json_schema ||= JsonSchema.parse!(schema_data)
        end

        def add_schema_to_document_store
          Dir.glob("#{schema_directory}/**/*.json").each do |path|
            schema_data = load_json_file(path)
            extra_schema = JsonSchema.parse!(schema_data)
            document_store.add_schema(extra_schema)
          end
        end

        def load_json(json)
          JSON.parse(json)
        rescue JSON::ParserError => ex
          raise InvalidSchemaError, ex.message
        end

        def load_json_file(path)
          load_json(File.read(path))
        rescue Errno::ENOENT
          raise MissingSchema, "No Schema file at #{schema_full_path}"
        end

        def require_json_schema!
          require 'json_schema'
        rescue LoadError
          raise LoadError, "You don't have json_schema installed in your application. Please add it to your Gemfile and run bundle install"
        end
      end
      class AssertResponseSchema < AssertSchema
        def initialize(*)
          super
          @payload = response_body
        end
      end
      class AssertRequestSchema < AssertSchema
        def initialize(*)
          super
          @payload = request_params
        end
      end
    end
  end
end
