// Copyright (C) 2010-2023 Joel Rosdahl and other contributors
//
// See doc/AUTHORS.adoc for a complete list of contributors.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program; if not, write to the Free Software Foundation, Inc., 51
// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

#include "../src/Args.hpp"
#include "../src/Config.hpp"
#include "../src/Context.hpp"
#include "../src/Util.hpp"
#include "../src/fmtmacros.hpp"
#include "TestUtil.hpp"
#include "argprocessing.hpp"

#include <Util.hpp>
#include <core/Statistic.hpp>
#include <core/wincompat.hpp>
#include <util/file.hpp>
#include <util/string.hpp>

#include "third_party/doctest.h"

#include <algorithm>

using core::Statistic;
using TestUtil::TestContext;

namespace {

std::string
get_root()
{
#ifndef _WIN32
  return "/";
#else
  char volume[4]; // "C:\"
  GetVolumePathName(Util::get_actual_cwd().c_str(), volume, sizeof(volume));
  volume[2] = '/'; // Since base directory is normalized to forward slashes
  return volume;
#endif
}

std::string
get_posix_path(const std::string& path)
{
#ifndef _WIN32
  return path;
#else
  std::string posix;

  // /-escape volume.
  if (path[1] == ':'
      && ((path[0] >= 'A' && path[0] <= 'Z')
          || (path[0] >= 'a' && path[0] <= 'z'))) {
    posix = "/" + path;
  } else {
    posix = path;
  }
  // Convert slashes.
  std::replace(posix.begin(), posix.end(), '\\', '/');
  return posix;
#endif
}

} // namespace

TEST_SUITE_BEGIN("argprocessing");

TEST_CASE("pass -fsyntax-only to compiler only")
{
  TestContext test_context;
  Context ctx;

  ctx.orig_args = Args::from_string("cc -c foo.c -fsyntax-only");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc");
  CHECK(result.extra_args_to_hash.to_string() == "-fsyntax-only");
  CHECK(result.compiler_args.to_string() == "cc -fsyntax-only -c");
}

TEST_CASE("dash_E_should_result_in_called_for_preprocessing")
{
  TestContext test_context;

  Context ctx;
  ctx.orig_args = Args::from_string("cc -c foo.c -E");

  util::write_file("foo.c", "");
  CHECK(process_args(ctx).error == Statistic::called_for_preprocessing);
}

TEST_CASE("dash_M_should_be_unsupported")
{
  TestContext test_context;

  Context ctx;
  ctx.orig_args = Args::from_string("cc -c foo.c -M");

  util::write_file("foo.c", "");
  CHECK(process_args(ctx).error == Statistic::unsupported_compiler_option);
}

TEST_CASE("dependency_args_to_preprocessor_if_run_second_cpp_is_false")
{
  TestContext test_context;
  const std::string dep_args =
    "-MD -MMD -MP -MF foo.d -MT mt1 -MT mt2 -MQ mq1 -MQ mq2 -Wp,-MP"
    " -Wp,-MT,wpmt -Wp,-MQ,wpmq -Wp,-MF,wpf";
  Context ctx;
  ctx.orig_args = Args::from_string("cc " + dep_args + " -c foo.c -o foo.o");
  util::write_file("foo.c", "");
  ctx.config.set_run_second_cpp(false);

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc " + dep_args);
  CHECK(result.extra_args_to_hash.to_string() == "");
  CHECK(result.compiler_args.to_string() == "cc -c");
}

TEST_CASE("dependency_args_to_compiler_if_run_second_cpp_is_true")
{
  TestContext test_context;
  const std::string dep_args =
    "-MD -MMD -MP -MF foo.d -MT mt1 -MT mt2 -MQ mq1 -MQ mq2 -Wp,-MP"
    " -Wp,-MT,wpmt -Wp,-MQ,wpmq -Wp,-MF,wpf";
  Context ctx;
  ctx.orig_args = Args::from_string("cc " + dep_args + " -c foo.c -o foo.o");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc");
  CHECK(result.extra_args_to_hash.to_string() == dep_args);
  CHECK(result.compiler_args.to_string() == "cc -c " + dep_args);
}

TEST_CASE("cpp_only_args_to_preprocessor_if_run_second_cpp_is_false")
{
  TestContext test_context;
  const std::string cpp_args =
    "-I. -idirafter . -iframework. -imacros . -imultilib . -include test.h"
    " -include-pch test.pch -iprefix . -iquote . -isysroot . -isystem ."
    " -iwithprefix . -iwithprefixbefore . -DTEST_MACRO -DTEST_MACRO2=1 -F."
    " -trigraphs -fworking-directory -fno-working-directory";
  const std::string dep_args =
    "-MD -MMD -MP -MF foo.d -MT mt1 -MT mt2 -MQ mq1 -MQ mq2 -Wp,-MP"
    " -Wp,-MT,wpmt -Wp,-MQ,wpmq -Wp,-MF,wpf";
  Context ctx;
  ctx.orig_args =
    Args::from_string("cc " + cpp_args + " " + dep_args + " -c foo.c -o foo.o");
  util::write_file("foo.c", "");
  ctx.config.set_run_second_cpp(false);

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string()
        == "cc " + cpp_args + " " + dep_args);
  CHECK(result.extra_args_to_hash.to_string() == "");
  CHECK(result.compiler_args.to_string() == "cc -c");
}

TEST_CASE(
  "cpp_only_args_to_preprocessor_and_compiler_if_run_second_cpp_is_true")
{
  TestContext test_context;
  const std::string cpp_args =
    "-I. -idirafter . -iframework. -imacros . -imultilib . -include test.h"
    " -include-pch test.pch -iprefix . -iquote . -isysroot . -isystem ."
    " -iwithprefix . -iwithprefixbefore . -DTEST_MACRO -DTEST_MACRO2=1 -F."
    " -trigraphs -fworking-directory -fno-working-directory";
  const std::string dep_args =
    "-MD -MMD -MP -MF foo.d -MT mt1 -MT mt2 -MQ mq1 -MQ mq2";
  Context ctx;
  ctx.orig_args =
    Args::from_string("cc " + cpp_args + " " + dep_args + " -c foo.c -o foo.o");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc " + cpp_args);
  CHECK(result.extra_args_to_hash.to_string() == dep_args);
  CHECK(result.compiler_args.to_string()
        == "cc " + cpp_args + " -c " + dep_args);
}

TEST_CASE(
  "dependency_args_that_take_an_argument_should_not_require_space_delimiter")
{
  TestContext test_context;
  const std::string dep_args = "-MMD -MFfoo.d -MT mt -MTmt -MQmq";
  Context ctx;
  ctx.orig_args = Args::from_string("cc -c " + dep_args + " foo.c -o foo.o");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc");
  CHECK(result.extra_args_to_hash.to_string() == dep_args);
  CHECK(result.compiler_args.to_string() == "cc -c " + dep_args);
}

TEST_CASE("MQ_flag_should_not_be_added_if_run_second_cpp_is_true")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("cc -c -MD foo.c -MF foo.d -o foo.o");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc");
  CHECK(result.extra_args_to_hash.to_string() == "-MD -MF foo.d");
  CHECK(result.compiler_args.to_string() == "cc -c -MD -MF foo.d");
}

TEST_CASE("MQ_flag_should_be_added_if_run_second_cpp_is_false")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("cc -c -MD foo.c -MF foo.d -o foo.o");
  util::write_file("foo.c", "");
  ctx.config.set_run_second_cpp(false);

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc -MD -MF foo.d -MQ foo.o");
  CHECK(result.extra_args_to_hash.to_string() == "");
  CHECK(result.compiler_args.to_string() == "cc -c");
}

TEST_CASE("MF_should_be_added_if_run_second_cpp_is_false")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("cc -c -MD foo.c -o foo.o");
  util::write_file("foo.c", "");
  ctx.config.set_run_second_cpp(false);

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc -MD -MF foo.d -MQ foo.o");
  CHECK(result.extra_args_to_hash.to_string() == "");
  CHECK(result.compiler_args.to_string() == "cc -c");
}

TEST_CASE("MF_should_not_be_added_if_run_second_cpp_is_true")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("cc -c -MD foo.c -o foo.o");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc");
  CHECK(result.extra_args_to_hash.to_string() == "-MD");
  CHECK(result.compiler_args.to_string() == "cc -c -MD");
}

TEST_CASE("equal_sign_after_MF_should_be_removed")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("cc -c -MF=path foo.c -o foo.o");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc");
  CHECK(result.extra_args_to_hash.to_string() == "-MFpath");
  CHECK(result.compiler_args.to_string() == "cc -c -MFpath");
}

TEST_CASE("sysroot_should_be_rewritten_if_basedir_is_used")
{
  TestContext test_context;

  Context ctx;

  util::write_file("foo.c", "");
  ctx.config.set_base_dir(get_root());
  std::string arg_string =
    FMT("cc --sysroot={}/foo/bar -c foo.c", ctx.actual_cwd);
  ctx.orig_args = Args::from_string(arg_string);

  const ProcessArgsResult result = process_args(ctx);
  CHECK(!result.error);
  CHECK(result.preprocessor_args[1] == "--sysroot=./foo/bar");
}

TEST_CASE(
  "sysroot_with_separate_argument_should_be_rewritten_if_basedir_is_used")
{
  TestContext test_context;

  Context ctx;

  util::write_file("foo.c", "");
  ctx.config.set_base_dir(get_root());
  std::string arg_string = FMT("cc --sysroot {}/foo -c foo.c", ctx.actual_cwd);
  ctx.orig_args = Args::from_string(arg_string);

  const ProcessArgsResult result = process_args(ctx);
  CHECK(!result.error);
  CHECK(result.preprocessor_args[1] == "--sysroot");
  CHECK(result.preprocessor_args[2] == "./foo");
}

TEST_CASE("MF_flag_with_immediate_argument_should_work_as_last_argument")
{
  TestContext test_context;

  Context ctx;
  ctx.orig_args =
    Args::from_string("cc -c foo.c -o foo.o -MMD -MT bar -MFfoo.d");

  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);
  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc");
  CHECK(result.extra_args_to_hash.to_string() == "-MMD -MT bar -MFfoo.d");
  CHECK(result.compiler_args.to_string() == "cc -c -MMD -MT bar -MFfoo.d");
}

TEST_CASE("MT_flag_with_immediate_argument_should_work_as_last_argument")
{
  TestContext test_context;

  Context ctx;
  ctx.orig_args =
    Args::from_string("cc -c foo.c -o foo.o -MMD -MFfoo.d -MT foo -MTbar");

  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);
  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc");
  CHECK(result.extra_args_to_hash.to_string()
        == "-MMD -MFfoo.d -MT foo -MTbar");
  CHECK(result.compiler_args.to_string()
        == "cc -c -MMD -MFfoo.d -MT foo -MTbar");
}

TEST_CASE("MQ_flag_with_immediate_argument_should_work_as_last_argument")
{
  TestContext test_context;

  Context ctx;
  ctx.orig_args =
    Args::from_string("cc -c foo.c -o foo.o -MMD -MFfoo.d -MQ foo -MQbar");

  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);
  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc");
  CHECK(result.extra_args_to_hash.to_string()
        == "-MMD -MFfoo.d -MQ foo -MQbar");
  CHECK(result.compiler_args.to_string()
        == "cc -c -MMD -MFfoo.d -MQ foo -MQbar");
}

TEST_CASE("MQ_flag_without_immediate_argument_should_not_add_MQobj")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("gcc -c -MD -MP -MFfoo.d -MQ foo.d foo.c");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "gcc");
  CHECK(result.extra_args_to_hash.to_string() == "-MD -MP -MFfoo.d -MQ foo.d");
  CHECK(result.compiler_args.to_string()
        == "gcc -c -MD -MP -MFfoo.d -MQ foo.d");
}

TEST_CASE("MT_flag_without_immediate_argument_should_not_add_MTobj")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("gcc -c -MD -MP -MFfoo.d -MT foo.d foo.c");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "gcc");
  CHECK(result.extra_args_to_hash.to_string() == "-MD -MP -MFfoo.d -MT foo.d");
  CHECK(result.compiler_args.to_string()
        == "gcc -c -MD -MP -MFfoo.d -MT foo.d");
}

TEST_CASE("MQ_flag_with_immediate_argument_should_not_add_MQobj")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("gcc -c -MD -MP -MFfoo.d -MQfoo.d foo.c");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "gcc");
  CHECK(result.extra_args_to_hash.to_string() == "-MD -MP -MFfoo.d -MQfoo.d");
  CHECK(result.compiler_args.to_string() == "gcc -c -MD -MP -MFfoo.d -MQfoo.d");
}

TEST_CASE("MT_flag_with_immediate_argument_should_not_add_MQobj")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("gcc -c -MD -MP -MFfoo.d -MTfoo.d foo.c");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "gcc");
  CHECK(result.extra_args_to_hash.to_string() == "-MD -MP -MFfoo.d -MTfoo.d");
  CHECK(result.compiler_args.to_string() == "gcc -c -MD -MP -MFfoo.d -MTfoo.d");
}

TEST_CASE(
  "isystem_flag_with_separate_arg_should_be_rewritten_if_basedir_is_used")
{
  TestContext test_context;

  Context ctx;

  util::write_file("foo.c", "");
  ctx.config.set_base_dir(get_root());
  std::string arg_string = FMT("cc -isystem {}/foo -c foo.c", ctx.actual_cwd);
  ctx.orig_args = Args::from_string(arg_string);

  const ProcessArgsResult result = process_args(ctx);
  CHECK(!result.error);
  CHECK(result.preprocessor_args[2] == "./foo");
}

TEST_CASE("isystem_flag_with_concat_arg_should_be_rewritten_if_basedir_is_used")
{
  TestContext test_context;

  Context ctx;

  util::write_file("foo.c", "");
  ctx.config.set_base_dir("/"); // posix
  // Windows path doesn't work concatenated.
  std::string cwd = get_posix_path(ctx.actual_cwd);
  std::string arg_string = FMT("cc -isystem{}/foo -c foo.c", cwd);
  ctx.orig_args = Args::from_string(arg_string);

  const ProcessArgsResult result = process_args(ctx);
  CHECK(!result.error);
  CHECK(result.preprocessor_args[1] == "-isystem./foo");
}

TEST_CASE("I_flag_with_concat_arg_should_be_rewritten_if_basedir_is_used")
{
  TestContext test_context;

  Context ctx;

  util::write_file("foo.c", "");
  ctx.config.set_base_dir("/"); // posix
  // Windows path doesn't work concatenated.
  std::string cwd = get_posix_path(ctx.actual_cwd);
  std::string arg_string = FMT("cc -I{}/foo -c foo.c", cwd);
  ctx.orig_args = Args::from_string(arg_string);

  const ProcessArgsResult result = process_args(ctx);
  CHECK(!result.error);
  CHECK(result.preprocessor_args[1] == "-I./foo");
}

TEST_CASE("debug_flag_order_with_known_option_first")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("cc -g1 -gsplit-dwarf foo.c -c");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc -g1 -gsplit-dwarf");
  CHECK(result.extra_args_to_hash.to_string() == "");
  CHECK(result.compiler_args.to_string() == "cc -g1 -gsplit-dwarf -c");
}

TEST_CASE("debug_flag_order_with_known_option_last")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string("cc -gsplit-dwarf -g1 foo.c -c");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc -gsplit-dwarf -g1");
  CHECK(result.extra_args_to_hash.to_string() == "");
  CHECK(result.compiler_args.to_string() == "cc -gsplit-dwarf -g1 -c");
}

TEST_CASE("options_not_to_be_passed_to_the_preprocessor")
{
  TestContext test_context;
  Context ctx;
  ctx.orig_args = Args::from_string(
    "cc -Wa,foo foo.c -g -c -DX -Werror -Xlinker fie -Xlinker,fum -Wno-error");
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cc -g -DX");
  CHECK(result.extra_args_to_hash.to_string()
        == "-Wa,foo -Werror -Xlinker fie -Xlinker,fum -Wno-error");
  CHECK(result.compiler_args.to_string()
        == "cc -g -Wa,foo -Werror -Xlinker fie -Xlinker,fum -Wno-error -DX -c");
}

TEST_CASE("cuda_option_file")
{
  TestContext test_context;
  Context ctx;
  ctx.config.set_compiler_type(CompilerType::nvcc);
  ctx.orig_args = Args::from_string("nvcc -optf foo.optf,bar.optf");
  util::write_file("foo.c", "");
  util::write_file("foo.optf", "-c foo.c -g -Wall -o");
  util::write_file("bar.optf", "out -DX");

  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "nvcc -g -Wall -DX");
  CHECK(result.extra_args_to_hash.to_string() == "");
  CHECK(result.compiler_args.to_string() == "nvcc -g -Wall -DX -c");
}

TEST_CASE("nvcc_warning_flags_short")
{
  // With -Werror. This should conflict with host's -Werror flag.
  TestContext test_context;
  Context ctx;
  ctx.config.set_compiler_type(CompilerType::nvcc);
  ctx.orig_args =
    Args::from_string("nvcc -Werror all-warnings -Xcompiler -Werror -c foo.cu");
  util::write_file("foo.cu", "");
  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "nvcc -Xcompiler -Werror");
  CHECK(result.extra_args_to_hash.to_string() == "-Werror all-warnings");
  CHECK(result.compiler_args.to_string()
        == "nvcc -Werror all-warnings -Xcompiler -Werror -c");
}

TEST_CASE("nvcc_warning_flags_long")
{
  // With --Werror. This shouldn't conflict with host's -Werror flag.
  TestContext test_context;
  Context ctx;
  ctx.config.set_compiler_type(CompilerType::nvcc);
  ctx.orig_args = Args::from_string(
    "nvcc --Werror all-warnings -Xcompiler -Werror -c foo.cu");
  util::write_file("foo.cu", "");
  const ProcessArgsResult result = process_args(ctx);

  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "nvcc -Xcompiler -Werror");
  CHECK(result.extra_args_to_hash.to_string() == "--Werror all-warnings");
  CHECK(result.compiler_args.to_string()
        == "nvcc --Werror all-warnings -Xcompiler -Werror -c");
}

TEST_CASE("-Xclang")
{
  TestContext test_context;
  Context ctx;
  ctx.config.set_compiler_type(CompilerType::clang);

  const std::string common_args =
    "-Xclang -fno-pch-timestamp"
    " -Xclang unsupported";

  const std::string color_diag = "-Xclang -fcolor-diagnostics";

  const std::string extra_args =
    "-Xclang -emit-pch"
    " -Xclang -emit-pth";

  const std::string pch_pth_variants =
    "-Xclang -include-pch pch_path1"
    " -Xclang -include-pch -Xclang pch_path2"
    " -Xclang -include-pth pth_path1"
    " -Xclang -include-pth -Xclang pth_path2";

  ctx.orig_args =
    Args::from_string("clang -c foo.c " + common_args + " " + color_diag + " "
                      + extra_args + " " + pch_pth_variants);
  util::write_file("foo.c", "");

  const ProcessArgsResult result = process_args(ctx);
  CHECK(result.preprocessor_args.to_string()
        == "clang " + common_args + " " + pch_pth_variants);
  CHECK(result.extra_args_to_hash.to_string() == extra_args);
  CHECK(result.compiler_args.to_string()
        == "clang " + common_args + " " + color_diag + " " + extra_args + " "
             + pch_pth_variants + " -c -fcolor-diagnostics");
}

TEST_CASE("-x")
{
  TestContext test_context;
  Context ctx;
  util::write_file("foo.c", "");

  SUBCASE("intel option")
  {
    // -xCODE1 (where CODE1 can be e.g. Host or CORE-AVX2, always starting with
    // an uppercase letter) is an ordinary Intel compiler option, not a language
    // specification.
    ctx.orig_args = Args::from_string("gcc -c foo.c -xCODE");

    const ProcessArgsResult result = process_args(ctx);
    CHECK(result.preprocessor_args.to_string() == "gcc -xCODE");
    CHECK(result.extra_args_to_hash.to_string() == "");
    CHECK(result.compiler_args.to_string() == "gcc -xCODE -c");
  }

  SUBCASE("compile .c as c++ (without space)")
  {
    ctx.orig_args = Args::from_string("gcc -xc++ -c foo.c");
    const ProcessArgsResult result = process_args(ctx);
    CHECK(ctx.args_info.actual_language == "c++");
    CHECK(result.preprocessor_args.to_string() == "gcc -x c++");
    CHECK(result.extra_args_to_hash.to_string() == "");
    CHECK(result.compiler_args.to_string() == "gcc -x c++ -c");
  }

  SUBCASE("compile .c as c++ (with space)")
  {
    ctx.orig_args = Args::from_string("gcc -x c++ -c foo.c");
    const ProcessArgsResult result = process_args(ctx);
    CHECK(ctx.args_info.actual_language == "c++");
    CHECK(result.preprocessor_args.to_string() == "gcc -x c++");
    CHECK(result.extra_args_to_hash.to_string() == "");
    CHECK(result.compiler_args.to_string() == "gcc -x c++ -c");
  }

  SUBCASE("compile .c as c++ (file first, no effect)")
  {
    ctx.orig_args = Args::from_string("gcc -c foo.c -x c++");
    const ProcessArgsResult result = process_args(ctx);
    CHECK(ctx.args_info.actual_language == "c");
    CHECK(result.preprocessor_args.to_string() == "gcc");
    CHECK(result.extra_args_to_hash.to_string() == "");
    CHECK(result.compiler_args.to_string() == "gcc -c");
  }

  SUBCASE("unknown -x option (lowercase)")
  {
    ctx.orig_args = Args::from_string("gcc -x unsupported_language -c foo.c");
    const ProcessArgsResult result = process_args(ctx);
    CHECK(result.error == Statistic::unsupported_source_language);
    CHECK(ctx.args_info.actual_language == "");
  }

  SUBCASE("UNKNOWN -x option (uppercase)")
  {
    ctx.orig_args = Args::from_string("gcc -x UNSUPPORTED_LANGUAGE -c foo.c");
    const ProcessArgsResult result = process_args(ctx);
    CHECK(result.error == Statistic::unsupported_source_language);
    CHECK(ctx.args_info.actual_language == "");
  }

  SUBCASE("missing param")
  {
    ctx.orig_args = Args::from_string("gcc -c foo.c -x");
    const ProcessArgsResult result = process_args(ctx);
    CHECK(result.error == Statistic::bad_compiler_arguments);
    CHECK(ctx.args_info.actual_language == "");
  }
}

// On macOS ctx.actual_cwd() typically starts with /Users which clashes with
// MSVC's /U option, so disable the test case there. This will be possible to
// improve when/if a compiler abstraction is introduced (issue #956).
TEST_CASE("MSVC options"
          * doctest::skip(util::starts_with(Util::get_actual_cwd(), "/U")))
{
  TestContext test_context;
  Context ctx;
  ctx.config.set_compiler_type(CompilerType::msvc);

  util::write_file("foo.c", "");

  ctx.orig_args = Args::from_string(
    FMT("cl.exe /Fobar.obj /c {}/foo.c /foobar", ctx.actual_cwd));
  const ProcessArgsResult result = process_args(ctx);
  CHECK(!result.error);
  CHECK(result.preprocessor_args.to_string() == "cl.exe /foobar");
  CHECK(result.compiler_args.to_string() == "cl.exe /foobar -c");
}

TEST_CASE("MSVC debug information format options")
{
  TestContext test_context;
  Context ctx;
  ctx.config.set_compiler_type(CompilerType::msvc);
  util::write_file("foo.c", "");

  SUBCASE("Only /Z7")
  {
    ctx.orig_args = Args::from_string("cl.exe /c foo.c /Z7");
    const ProcessArgsResult result = process_args(ctx);
    REQUIRE(!result.error);
    CHECK(result.preprocessor_args.to_string() == "cl.exe /Z7");
    CHECK(result.compiler_args.to_string() == "cl.exe /Z7 -c");
  }

  SUBCASE("Only /Zi")
  {
    ctx.orig_args = Args::from_string("cl.exe /c foo.c /Zi");
    const ProcessArgsResult result = process_args(ctx);
    CHECK(result.error == Statistic::unsupported_compiler_option);
  }

  SUBCASE("Only /ZI")
  {
    ctx.orig_args = Args::from_string("cl.exe /c foo.c /ZI");
    const ProcessArgsResult result = process_args(ctx);
    CHECK(result.error == Statistic::unsupported_compiler_option);
  }

  SUBCASE("/Z7 + /Zi")
  {
    ctx.orig_args = Args::from_string("cl.exe /Z7 /c foo.c /Zi");
    const ProcessArgsResult result = process_args(ctx);
    CHECK(result.error == Statistic::unsupported_compiler_option);
  }

  SUBCASE("/Zi + /Z7")
  {
    ctx.orig_args = Args::from_string("cl.exe /Zi /c foo.c /Z7");
    const ProcessArgsResult result = process_args(ctx);
    REQUIRE(!result.error);
    CHECK(result.preprocessor_args.to_string() == "cl.exe /Zi /Z7");
    CHECK(result.compiler_args.to_string() == "cl.exe /Zi /Z7 -c");
  }
}

// Check that clang-cl debug information is parsed different,
// since for clang-cl /Zi and /Z7 is the same!
TEST_CASE("ClangCL Debug information options")
{
  TestContext test_context;
  Context ctx;
  ctx.config.set_compiler_type(CompilerType::clang_cl);
  util::write_file("foo.c", "");

  SUBCASE("/Z7")
  {
    ctx.orig_args = Args::from_string("clang-cl.exe /c foo.c /Z7");
    const ProcessArgsResult result = process_args(ctx);
    REQUIRE(!result.error);
    CHECK(result.preprocessor_args.to_string() == "clang-cl.exe /Z7");
  }

  SUBCASE("/Zi")
  {
    ctx.orig_args = Args::from_string("clang-cl.exe /c foo.c /Zi");
    const ProcessArgsResult result = process_args(ctx);
    REQUIRE(!result.error);
    CHECK(result.preprocessor_args.to_string() == "clang-cl.exe /Zi");
  }
}

TEST_SUITE_END();
