# frozen_string_literal: true

require "selenium-webdriver"

module DiscourseTheme
  module CliCommands
    class Rspec
      DISCOURSE_TEST_DOCKER_CONTAINER_NAME_PREFIX = "discourse_theme_test"
      DISCOURSE_THEME_TEST_TMP_DIR = "/tmp/.discourse_theme_test"
      SELENIUM_HEADFUL_ENV = "SELENIUM_HEADLESS=0"

      class << self
        def discourse_test_docker_container_name
          "#{DISCOURSE_TEST_DOCKER_CONTAINER_NAME_PREFIX}_#{DiscourseTheme::VERSION}"
        end

        def run(settings:, dir:, args:, reset: false)
          settings.local_discourse_directory = nil if reset

          spec_path = "/spec"
          index = dir.index(spec_path)

          if index
            spec_path = dir[index..-1]
            dir = dir[0..index - 1]
          end

          spec_directory = File.join(dir, "/spec")

          unless Dir.exist?(spec_directory)
            raise DiscourseTheme::ThemeError.new "'#{spec_directory} does not exist"
          end

          configure_local_directory(settings)

          headless = !args.delete("--headful")

          if settings.local_discourse_directory.empty?
            run_tests_with_docker(
              File.basename(dir),
              spec_directory,
              spec_path,
              headless: headless,
              verbose: !!args.delete("--verbose"),
              rebuild: !!args.delete("--rebuild"),
            )
          else
            run_tests_locally(
              settings.local_discourse_directory,
              File.join(dir, spec_path),
              headless: headless,
            )
          end
        end

        private

        def execute(command:, message: nil, exit_on_error: true, stream: false)
          UI.progress(message) if message

          success = false
          output = +""

          Open3.popen2e(command) do |stdin, stdout_and_stderr, wait_thr|
            Thread.new do
              stdout_and_stderr.each do |line|
                puts line if stream
                output << line
              end
            end

            exit_status = wait_thr.value
            success = exit_status.success?

            unless success
              UI.error "Error occured while running: `#{command}`:\n\n#{output}" unless stream
              exit 1 if exit_on_error
            end
          end

          output
        end

        def run_tests_locally(local_directory, spec_path, headless: false)
          UI.progress(
            "Running RSpec tests using local Discourse repository located at '#{local_directory}'...",
          )

          Kernel.exec(
            ENV,
            "cd #{local_directory} && #{headless ? "" : SELENIUM_HEADFUL_ENV} bundle exec rspec #{spec_path}",
          )
        end

        def run_tests_with_docker(
          theme_directory_name,
          spec_directory,
          spec_path,
          headless: false,
          verbose: false,
          rebuild: false
        )
          image = "discourse/discourse_test:release"
          UI.progress("Running RSpec tests using '#{image}' Docker image...")

          unless Dir.exist?(DISCOURSE_THEME_TEST_TMP_DIR)
            FileUtils.mkdir_p DISCOURSE_THEME_TEST_TMP_DIR
          end

          # Checks if the container is running
          container_name = discourse_test_docker_container_name
          is_running = false

          if !(
               output =
                 execute(
                   command: "docker ps -a --filter name=#{container_name} --format '{{json .}}'",
                 )
             ).empty?
            is_running = JSON.parse(output)["State"] == "running"
          end

          if !is_running || rebuild
            # Stop older versions of Docker container
            existing_docker_container_ids =
              execute(
                command:
                  "docker ps -a -q --filter name=#{DISCOURSE_TEST_DOCKER_CONTAINER_NAME_PREFIX}",
              ).split("\n").join(" ")

            if !existing_docker_container_ids.empty?
              execute(command: "docker stop #{existing_docker_container_ids}")
              execute(command: "docker rm -f #{existing_docker_container_ids}")
            end

            execute(
              command: <<~CMD.squeeze(" "),
              docker run -d \
                -p 31337:31337 \
                --add-host host.docker.internal:host-gateway \
                --entrypoint=/sbin/boot \
                --name=#{container_name} \
                --pull=always \
                -v #{DISCOURSE_THEME_TEST_TMP_DIR}:/tmp \
                #{image}
            CMD
              message: "Creating #{image} Docker container...",
              stream: verbose,
            )

            execute(
              command:
                "docker exec -u discourse:discourse #{container_name} ruby script/docker_test.rb --no-tests --checkout-ref origin/tests-passed",
              message: "Checking out latest Discourse source code...",
              stream: verbose,
            )

            execute(
              command:
                "docker exec -e SKIP_MULTISITE=1 -u discourse:discourse #{container_name} bundle exec rake docker:test:setup",
              message: "Setting up Discourse test environment...",
              stream: verbose,
            )

            execute(
              command: "docker exec -u discourse:discourse #{container_name} bin/ember-cli --build",
              message: "Building Ember CLI assets...",
              stream: verbose,
            )
          end

          rspec_envs = []

          if !headless
            container_ip =
              execute(
                command:
                  "docker inspect #{container_name} --format '{{.NetworkSettings.IPAddress}}'",
              ).chomp("\n")

            service =
              start_chromedriver(allowed_origin: "host.docker.internal", allowed_ip: container_ip)

            rspec_envs.push(SELENIUM_HEADFUL_ENV)
            rspec_envs.push("CAPYBARA_SERVER_HOST=0.0.0.0")
            rspec_envs.push(
              "CAPYBARA_REMOTE_DRIVER_URL=http://host.docker.internal:#{service.uri.port}",
            )
          end

          rspec_envs = rspec_envs.map { |env| "-e #{env}" }.join(" ")

          begin
            tmp_theme_directory = File.join(DISCOURSE_THEME_TEST_TMP_DIR, theme_directory_name)
            FileUtils.mkdir_p(tmp_theme_directory) if !Dir.exist?(tmp_theme_directory)
            FileUtils.cp_r(spec_directory, File.join(tmp_theme_directory))

            execute(
              command:
                "docker exec #{rspec_envs} -t -u discourse:discourse #{container_name} bundle exec rspec #{File.join("/tmp", theme_directory_name, spec_path)}".squeeze(
                  " ",
                ),
              stream: true,
            )
          ensure
            FileUtils.rm_rf(File.join(tmp_theme_directory, "/spec"))
          end
        end

        def configure_local_directory(settings)
          return if settings.local_discourse_directory_configured?

          should_configure_local_directory =
            UI.yes?(
              "Would you like to configure a local Discourse repository used to run the RSpec tests? If you select 'n', the tests will be run using a Docker container.",
            )

          if should_configure_local_directory
            local_discourse_directory =
              UI.ask("Please enter the path to the local Discourse directory:")

            unless Dir.exist?(local_discourse_directory)
              raise DiscourseTheme::ThemeError.new "'#{local_discourse_directory} does not exist"
            end

            unless File.exist?("#{local_discourse_directory}/lib/discourse.rb")
              raise DiscourseTheme::ThemeError.new "'#{local_discourse_directory} is not a Discourse repository"
            end

            settings.local_discourse_directory = local_discourse_directory
          else
            settings.local_discourse_directory = ""
          end
        end

        def start_chromedriver(allowed_ip:, allowed_origin:)
          service = Selenium::WebDriver::Service.chrome
          options = Selenium::WebDriver::Options.chrome
          service.executable_path = Selenium::WebDriver::DriverFinder.path(options, service.class)
          service.args = ["--allowed-ips=#{allowed_ip}", "--allowed-origins=#{allowed_origin}"]
          service.launch
        end
      end
    end
  end
end