# frozen_string_literal: true

require_relative "../../tools/strict_warnings"

$:.unshift File.expand_path("lib", __dir__)

require "active_support/core_ext/kernel/reporting"

# These are the normal settings that will be set up by Railties
# TODO: Have these tests support other combinations of these values
silence_warnings do
  Encoding.default_internal = Encoding::UTF_8
  Encoding.default_external = Encoding::UTF_8
end

PROCESS_COUNT = (ENV["MT_CPU"] || 4).to_i

require "active_support/testing/autorun"
require "abstract_controller"
require "abstract_controller/railties/routes_helpers"
require "action_controller"
require "action_view"
require "action_view/testing/resolvers"
require "action_dispatch"
require "active_support/dependencies"
require "active_model"
require "zeitwerk"

ActiveSupport::Cache.format_version = 7.1

module Rails
  class << self
    def env
      @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
    end

    def root; end
  end
end

module ActionPackTestSuiteUtils
  def self.require_helpers(helpers_dirs)
    Array(helpers_dirs).each do |helpers_dir|
      Dir.glob("#{helpers_dir}/**/*_helper.rb") do |helper_file|
        require helper_file
      end
    end
  end
end

ActionPackTestSuiteUtils.require_helpers("#{__dir__}/fixtures/helpers")
ActionPackTestSuiteUtils.require_helpers("#{__dir__}/fixtures/alternate_helpers")

Thread.abort_on_exception = true

# Show backtraces for deprecated behavior for quicker cleanup.
ActionController.deprecator.debug = true
ActionDispatch.deprecator.debug = true

# Disable available locale checks to avoid warnings running the test suite.
I18n.enforce_available_locales = false

FIXTURE_LOAD_PATH = File.join(__dir__, "fixtures")

SharedTestRoutes = ActionDispatch::Routing::RouteSet.new

SharedTestRoutes.draw do
  ActionDispatch.deprecator.silence do
    get ":controller(/:action)"
  end
end

module ActionDispatch
  module SharedRoutes
    def before_setup
      @routes = Routing::RouteSet.new
      ActionDispatch.deprecator.silence do
        @routes.draw { get ":controller(/:action)" }
      end
      super
    end
  end
end

module ActiveSupport
  class TestCase
    if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0
      parallelize(workers: PROCESS_COUNT)
    end
  end
end

class RoutedRackApp
  class Config < Struct.new(:middleware)
  end

  attr_reader :routes

  def initialize(routes, &blk)
    @routes = routes
    @stack = ActionDispatch::MiddlewareStack.new(&blk)
    @app = @stack.build(@routes)
  end

  def call(env)
    @app.call(env)
  end

  def config
    Config.new(@stack)
  end
end

class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
  def self.build_app(routes = nil)
    RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
      middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
      middleware.use ActionDispatch::DebugExceptions
      middleware.use ActionDispatch::ActionableExceptions
      middleware.use ActionDispatch::Callbacks
      middleware.use ActionDispatch::Cookies
      middleware.use ActionDispatch::Flash
      middleware.use Rack::MethodOverride
      middleware.use Rack::Head
      yield(middleware) if block_given?
    end
  end

  self.app = build_app

  app.routes.draw do
    ActionDispatch.deprecator.silence do
      get ":controller(/:action)"
    end
  end

  class DeadEndRoutes < ActionDispatch::Routing::RouteSet
    # Stub Rails dispatcher so it does not get controller references and
    # simply return the controller#action as Rack::Body.
    class NullController < ::ActionController::Metal
      def self.dispatch(action, req, res)
        [200, { "Content-Type" => "text/html" }, ["#{req.params[:controller]}##{action}"]]
      end
    end

    class NullControllerRequest < ActionDispatch::Request
      def controller_class
        NullController
      end
    end

    def make_request(env)
      NullControllerRequest.new env
    end
  end

  def self.stub_controllers(config = ActionDispatch::Routing::RouteSet::DEFAULT_CONFIG)
    yield DeadEndRoutes.new(config)
  end

  def with_autoload_path(path)
    path = File.join(__dir__, "fixtures", path)
    Zeitwerk.with_loader do |loader|
      loader.push_dir(path)
      loader.setup
      yield
    ensure
      loader.unload
    end
  end
end

# Temporary base class
class Rack::TestCase < ActionDispatch::IntegrationTest
  def self.testing(klass = nil)
    if klass
      @testing = "/#{klass.name.underscore}".delete_suffix("_controller")
    else
      @testing
    end
  end

  def get(thing, *args, **options)
    if thing.is_a?(Symbol)
      super("#{self.class.testing}/#{thing}", *args, **options)
    else
      super
    end
  end

  def assert_body(body)
    assert_equal body, Array(response.body).join
  end

  def assert_status(code)
    assert_equal code, response.status
  end

  def assert_response(body, status = 200, headers = {})
    assert_body body
    assert_status status
    headers.each do |header, value|
      assert_header header, value
    end
  end

  def assert_content_type(type)
    assert_equal type, response.headers["Content-Type"]
  end

  def assert_header(name, value)
    assert_equal value, response.headers[name]
  end
end

module ActionController
  class API
    extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes)
  end

  class Base
    # This stub emulates the Railtie including the URL helpers from a Rails application
    extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes)
    include SharedTestRoutes.mounted_helpers

    self.view_paths = FIXTURE_LOAD_PATH

    def self.test_routes(&block)
      routes = ActionDispatch::Routing::RouteSet.new
      routes.draw(&block)
      include routes.url_helpers
      routes
    end
  end

  class TestCase
    include ActionDispatch::TestProcess
    include ActionDispatch::SharedRoutes
  end
end

class ::ApplicationController < ActionController::Base
end

module ActionDispatch
  class DebugExceptions
    private
      remove_method :stderr_logger
      # Silence logger
      def stderr_logger
        nil
      end
  end
end

module ActionDispatch
  module RoutingVerbs
    def send_request(uri_or_host, method, path)
      host = uri_or_host.host unless path
      path ||= uri_or_host.path

      params = { "PATH_INFO" => path,
                 "REQUEST_METHOD" => method,
                 "HTTP_HOST" => host }

      routes.call(params)
    end

    def request_path_params(path, options = {})
      method = options[:method] || "GET"
      resp = send_request URI("http://localhost" + path), method.to_s.upcase, nil
      status = resp.first
      if status == 404
        raise ActionController::RoutingError, "No route matches #{path.inspect}"
      end
      controller.request.path_parameters
    end

    def get(uri_or_host, path = nil)
      send_request(uri_or_host, "GET", path)[2].join
    end

    def post(uri_or_host, path = nil)
      send_request(uri_or_host, "POST", path)[2].join
    end

    def put(uri_or_host, path = nil)
      send_request(uri_or_host, "PUT", path)[2].join
    end

    def delete(uri_or_host, path = nil)
      send_request(uri_or_host, "DELETE", path)[2].join
    end

    def patch(uri_or_host, path = nil)
      send_request(uri_or_host, "PATCH", path)[2].join
    end
  end
end

module RoutingTestHelpers
  def url_for(set, options)
    route_name = options.delete :use_route
    set.url_for options.merge(only_path: true), route_name
  end

  def make_set(strict = true)
    tc = self
    TestSet.new ->(c) { tc.controller = c }, strict
  end

  class TestSet < ActionDispatch::Routing::RouteSet
    class Request < DelegateClass(ActionDispatch::Request)
      def initialize(target, helpers, block, strict)
        super(target)
        @helpers = helpers
        @block = block
        @strict = strict
      end

      def controller_class
        helpers = @helpers
        block = @block
        Class.new(@strict ? super : ActionController::Base) {
          include helpers
          define_method(:process) { |name| block.call(self) }
          def to_a; [200, {}, []]; end
        }
      end
    end

    attr_reader :strict

    def initialize(block, strict = false)
      @block = block
      @strict = strict
      super()
    end

    private
      def make_request(env)
        Request.new super, url_helpers, @block, strict
      end
  end
end

class ResourcesController < ActionController::Base
  def index() head :ok end
  alias_method :show, :index
end

class CommentsController < ResourcesController; end
class AccountsController < ResourcesController; end
class ImagesController < ResourcesController; end

require "active_support/testing/method_call_assertions"

class ActiveSupport::TestCase
  include ActiveSupport::Testing::MethodCallAssertions
end

module CookieAssertions
  def parse_set_cookie_attributes(fields, attributes = {})
    if fields.is_a?(String)
      fields = fields.split(";").map(&:strip)
    end

    fields.each do |field|
      key, value = field.split("=", 2)

      # Normalize the key to lowercase:
      key.downcase!

      if value
        value.downcase!
        attributes[key] = value
      else
        attributes[key] = true
      end
    end

    attributes
  end

  # Parse the set-cookie header and return a hash of cookie names and values.
  #
  # Example:
  #   set_cookies = headers["set-cookie"]
  #   parse_set_cookies_headers(set_cookies)
  def parse_set_cookies_headers(set_cookies)
    if set_cookies.is_a?(String)
      set_cookies = set_cookies.split("\n")
    end

    cookies = {}

    set_cookies&.each do |cookie_string|
      attributes = {}

      fields = cookie_string.split(";").map(&:strip)

      # The first one is the cookie name:
      name, value = fields.shift.split("=", 2)

      attributes[:value] = value

      cookies[name] = parse_set_cookie_attributes(fields, attributes)
    end

    cookies
  end

  def assert_set_cookie_attributes(name, attributes, header = @response.headers["Set-Cookie"])
    cookies = parse_set_cookies_headers(header)
    attributes = parse_set_cookie_attributes(attributes) if attributes.is_a?(String)

    assert cookies.key?(name), "No cookie found with the name '#{name}', found cookies: #{cookies.keys.join(', ')}"
    cookie = cookies[name]

    attributes.each do |key, value|
      assert cookie.key?(key), "No attribute '#{key}' found for cookie '#{name}'"
      assert_equal value, cookie[key]
    end
  end

  def assert_not_set_cookie_attributes(name, attributes, header = @response.headers["Set-Cookie"])
    cookies = parse_set_cookies_headers(header)
    attributes = parse_set_cookie_attributes(attributes) if attributes.is_a?(String)

    assert cookies.key?(name), "No cookie found with the name '#{name}'"
    cookie = cookies[name]

    attributes.each do |key, value|
      if value == true
        assert_nil cookie[key]
      else
        assert_not_equal value, cookie[key]
      end
    end
  end

  def assert_set_cookie_header(expected, header = @response.headers["Set-Cookie"])
    # In Rack v2, this is newline delimited. In Rack v3, this is an array.
    # Normalize the comparison so that we can assert equality in both cases.

    if header.is_a?(String)
      header = header.split("\n").sort
    end

    if expected.is_a?(String)
      expected = expected.split("\n").sort
    end

    # While not strictly speaking correct, this is probably good enough for now:
    header = parse_set_cookies_headers(header)
    expected = parse_set_cookies_headers(expected)

    expected.each do |key, value|
      assert_equal value, header[key]
    end
  end

  def assert_not_set_cookie_header(expected, header = @response.headers["Set-Cookie"])
    if header.is_a?(String)
      header = header.split("\n").sort
    end

    if expected.is_a?(String)
      expected = expected.split("\n").sort
    end

    # While not strictly speaking correct, this is probably good enough for now:
    header = parse_set_cookies_headers(header)

    expected.each do |name|
      assert_not_includes(header, name)
    end
  end
end

module HeadersAssertions
  def normalize_headers(headers)
    headers.transform_keys(&:downcase)
  end

  def assert_headers(expected, actual = @response.headers)
    actual = normalize_headers(actual)
    expected.each do |key, value|
      assert_equal value, actual[key]
    end
  end

  def assert_header(key, value, actual = @response.headers)
    actual = normalize_headers(actual)
    assert_equal value, actual[key]
  end

  def assert_not_header(key, actual = @response.headers)
    actual = normalize_headers(actual)
    assert_not_includes(actual, key)
  end

  # This works for most headers, but not all, e.g. `set-cookie`.
  def normalized_join_header(header)
    header.is_a?(Array) ? header.join(",") : header
  end

  def assert_header_value(expected, header)
    header = normalized_join_header(header)
    assert_equal header, expected
  end
end

class DrivenByRackTest < ActionDispatch::SystemTestCase
  driven_by :rack_test
end

class DrivenBySeleniumWithChrome < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :chrome
end

class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome
end

class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_firefox
end

require_relative "../../tools/test_common"
