defmodule Mix.Tasks.Workspace.RunTest do
  use ExUnit.Case

  require Workspace.TestUtils

  import ExUnit.CaptureIO
  import Workspace.TestUtils

  alias Mix.Tasks.Workspace.Run, as: RunTask

  setup do
    Application.put_env(:elixir, :ansi_enabled, false)
  end

  @default_run_task [
    "-t",
    "format",
    "--",
    "--check-formatted",
    "mix.exs"
  ]

  describe "sanity checks of common cli arguments" do
    @tag :tmp_dir
    test "runs only on the selected projects", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "--workspace-path",
          tmp_dir,
          "-p",
          "package_a",
          "-p",
          "package_b",
          "--dry-run" | @default_run_task
        ]

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(captured, [
          "Running task in 2 workspace projects",
          "==> :package_a - mix format --check-formatted mix.exs",
          "==> :package_b - mix format --check-formatted mix.exs"
        ])

        args = [
          "--workspace-path",
          tmp_dir,
          "-e",
          "package_a",
          "-e",
          "package_b",
          "--dry-run" | @default_run_task
        ]

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(captured, [
          "Running task in 9 workspace projects",
          "==> :package_c - mix format --check-formatted mix.exs",
          "==> :package_d - mix format --check-formatted mix.exs",
          "==> :package_e - mix format --check-formatted mix.exs",
          "==> :package_f - mix format --check-formatted mix.exs",
          "==> :package_g - mix format --check-formatted mix.exs",
          "==> :package_h - mix format --check-formatted mix.exs",
          "==> :package_i - mix format --check-formatted mix.exs",
          "==> :package_j - mix format --check-formatted mix.exs",
          "==> :package_k - mix format --check-formatted mix.exs"
        ])
      end)
    end

    @tag :tmp_dir
    test "with postorder order set", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "--workspace-path",
          tmp_dir,
          "--order",
          "postorder",
          "--dry-run" | @default_run_task
        ]

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        order =
          captured
          |> String.split("\n")
          |> Enum.filter(&String.starts_with?(&1, "==> :package"))
          |> Enum.map(fn line ->
            ["==>", ":" <> project | _rest] = String.split(line, " ")
            String.to_existing_atom(project)
          end)
          |> Enum.with_index()

        # since the order is not deterministic we do some sanity checks based
        # on the graph topology
        assert order[:package_g] < order[:package_f]
        assert order[:package_g] < order[:package_b]
        assert order[:package_b] < order[:package_a]
        assert order[:package_e] < order[:package_c]
        assert order[:package_d] < order[:package_a]
        assert order[:package_d] < order[:package_h]
        assert order[:package_j] < order[:package_i]
      end)
    end

    @tag :tmp_dir
    test "with verbose on skipped projects are reported", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "--workspace-path",
          tmp_dir,
          "-p",
          "package_a",
          "--dry-run",
          "--verbose" | @default_run_task
        ]

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(
          captured,
          [
            "Running task in 1 workspace projects",
            "==> :package_a - mix format --check-formatted mix.exs",
            "==> :package_b - skipped",
            "==> :package_c - skipped",
            "==> :package_d - skipped",
            "==> :package_e - skipped",
            "==> :package_f - skipped",
            "==> :package_g - skipped",
            "==> :package_h - skipped",
            "==> :package_i - skipped",
            "==> :package_j - skipped",
            "==> :package_k - skipped"
          ]
        )
      end)
    end
  end

  describe "codebase changes flags" do
    @tag :tmp_dir
    test "with affected flag only affected projects are executed", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(
        tmp_dir,
        [],
        :default,
        fn ->
          # in a codebase with no changes
          captured =
            capture_io(fn -> RunTask.run(["--affected", "--dry-run" | @default_run_task]) end)

          assert_cli_output_match(captured, ["No matching projects for the given options"])

          # runs only on affected projects
          Workspace.Test.modify_project(tmp_dir, "package_d")
          Workspace.Test.modify_project(tmp_dir, "package_e")

          captured =
            capture_io(fn -> RunTask.run(["--affected", "--dry-run" | @default_run_task]) end)

          assert_cli_output_match(
            captured,
            [
              "Running task in 5 workspace projects",
              "==> :package_a - mix format --check-formatted mix.exs",
              "==> :package_c - mix format --check-formatted mix.exs",
              "==> :package_d - mix format --check-formatted mix.exs",
              "==> :package_e - mix format --check-formatted mix.exs",
              "==> :package_h - mix format --check-formatted mix.exs"
            ],
            partial: true
          )
        end,
        git: true,
        cd: true
      )
    end

    @tag :tmp_dir
    test "with modified flag only modified flags are executed", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(
        tmp_dir,
        [],
        :default,
        fn ->
          # in a codebase with no changes
          captured = capture_io(fn -> RunTask.run(["--modified" | @default_run_task]) end)

          assert_cli_output_match(captured, ["No matching projects for the given options"])

          # runs only on affected projects
          Workspace.Test.modify_project(tmp_dir, "package_d")
          Workspace.Test.modify_project(tmp_dir, "package_e")

          captured = capture_io(fn -> RunTask.run(["--modified" | @default_run_task]) end)

          assert_cli_output_match(captured, [
            "Running task in 2 workspace projects",
            "==> :package_d - mix format --check-formatted mix.exs",
            ":package_d mix format --check-formatted mix.exs succeeded [",
            "==> :package_e - mix format --check-formatted mix.exs",
            ":package_e mix format --check-formatted mix.exs succeeded ["
          ])
        end,
        git: true,
        cd: true
      )
    end

    @tag :tmp_dir
    test "with root-only flag only roots are triggered", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(
        tmp_dir,
        [],
        :default,
        fn ->
          # runs only on affected projects
          captured =
            capture_io(fn -> RunTask.run(["--only-roots", "--dry-run" | @default_run_task]) end)

          assert_cli_output_match(captured, [
            "Running task in 4 workspace projects",
            "==> :package_a - mix format --check-formatted mix.exs",
            "==> :package_h - mix format --check-formatted mix.exs",
            "==> :package_i - mix format --check-formatted mix.exs",
            "==> :package_k - mix format --check-formatted mix.exs"
          ])
        end,
        cd: true
      )
    end

    @tag :tmp_dir
    test "with root-only and affected only affected roots are triggered", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(
        tmp_dir,
        [],
        :default,
        fn ->
          Workspace.Test.modify_project(tmp_dir, "package_d")
          Workspace.Test.modify_project(tmp_dir, "package_e")

          # runs only on affected projects
          captured =
            capture_io(fn ->
              RunTask.run(["--affected", "--only-roots", "--dry-run" | @default_run_task])
            end)

          assert_cli_output_match(captured, [
            "Running task in 2 workspace projects",
            "==> :package_a - mix format --check-formatted mix.exs",
            "==> :package_h - mix format --check-formatted mix.exs"
          ])
        end,
        git: true,
        cd: true
      )
    end

    @tag :tmp_dir
    test "with show-status displays the status of each project", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(
        tmp_dir,
        [],
        :default,
        fn ->
          Workspace.Test.modify_project(tmp_dir, "package_d")
          Workspace.Test.modify_project(tmp_dir, "package_e")

          captured =
            capture_io(fn -> RunTask.run(["--show-status", "--dry-run" | @default_run_task]) end)

          assert_cli_output_match(
            captured,
            [
              "Running task in 11 workspace projects",
              "==> :package_a ● - mix format --check-formatted mix.exs",
              "==> :package_b ✔ - mix format --check-formatted mix.exs",
              "==> :package_c ● - mix format --check-formatted mix.exs",
              "==> :package_d ✚ - mix format --check-formatted mix.exs",
              "==> :package_e ✚ - mix format --check-formatted mix.exs",
              "==> :package_f ✔ - mix format --check-formatted mix.exs",
              "==> :package_g ✔ - mix format --check-formatted mix.exs",
              "==> :package_h ● - mix format --check-formatted mix.exs",
              "==> :package_i ✔ - mix format --check-formatted mix.exs",
              "==> :package_j ✔ - mix format --check-formatted mix.exs",
              "==> :package_k ✔ - mix format --check-formatted mix.exs"
            ],
            partial: true
          )
        end,
        git: true,
        cd: true
      )
    end

    @tag :tmp_dir
    test "on a repo with no working tree changes", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(
        tmp_dir,
        [],
        :default,
        fn ->
          Workspace.Test.modify_project(tmp_dir, "package_c")
          Workspace.Test.commit_changes(tmp_dir)

          captured = capture_io(fn -> RunTask.run(["--affected" | @default_run_task]) end)

          assert_cli_output_match(captured, ["No matching projects for the given options"])

          # with base and head set
          captured =
            capture_io(fn ->
              RunTask.run([
                "--affected",
                "--base",
                "HEAD~1",
                "--head",
                "HEAD",
                "--dry-run" | @default_run_task
              ])
            end)

          assert_cli_output_match(captured, [
            "Running task in 2 workspace projects",
            "==> :package_a - mix format --check-formatted mix.exs",
            "==> :package_c - mix format --check-formatted mix.exs"
          ])
        end,
        git: true,
        cd: true
      )
    end
  end

  describe "environment variables" do
    @tag :tmp_dir
    test "raises if improper configuration of environment variables", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(
        tmp_dir,
        [],
        :default,
        fn ->
          args = ["-p", "package_default_a", "--verbose", "--env-var", "FOO" | @default_run_task]

          expected_message =
            "invalid environment variable definition, " <>
              "it should be of the form ENV_VAR_NAME=value, got: FOO"

          assert_raise Mix.Error, expected_message, fn ->
            RunTask.run(args)
          end
        end,
        cd: true
      )
    end

    @tag :tmp_dir
    test "properly sets the evn variables", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        assert System.get_env("FOO") == nil

        args = [
          "-p",
          "package_a",
          "-t",
          "cmd",
          "--workspace-path",
          tmp_dir,
          "--env-var",
          "FOO=bar",
          "--",
          "echo",
          "$FOO"
        ]

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(captured, [
          "Running task in 1 workspace projects",
          "==> :package_a - mix cmd echo $FOO",
          "bar",
          ":package_a mix cmd echo $FOO succeeded ["
        ])

        assert System.get_env("FOO") == nil
      end)
    end
  end

  describe "exit status" do
    @tag :tmp_dir
    test "if execution on a project fails the command raises", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "-p",
          "package_a,package_b",
          "-t",
          "cmd",
          "--workspace-path",
          tmp_dir,
          "--",
          "exit",
          "1"
        ]

        expected_message = """
        mix workspace.run failed - errors detected in 2 executions
        failed projects - [:package_a, :package_b]
        """

        captured =
          assert_raise_and_capture_io(Mix.Error, expected_message, fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(
          captured,
          [
            "==> :package_a - mix cmd exit 1",
            "** (exit) 1",
            ":package_a mix cmd exit 1 failed with 1",
            "==> :package_b - mix cmd exit 1",
            "** (exit) 1",
            ":package_b mix cmd exit 1 failed with 1"
          ],
          partial: true
        )
      end)
    end

    @tag :tmp_dir
    test "with --early-stop set", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "-p",
          "package_a",
          "-p",
          "package_b",
          "-t",
          "cmd",
          "--early-stop",
          "--workspace-path",
          tmp_dir,
          "--",
          "exit",
          "1"
        ]

        expected_message = "--early-stop is set - terminating workspace.run"

        captured =
          assert_raise_and_capture_io(Mix.Error, expected_message, fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(
          captured,
          [
            "==> :package_a - mix cmd exit 1",
            "** (exit) 1",
            ":package_a mix cmd exit 1 failed with 1"
          ],
          partial: true
        )

        refute_cli_output_match(captured, [
          "==> :package_b - mix cmd exit 1",
          ":package_b mix cmd exit 1 failed with 1"
        ])
      end)
    end

    @tag :tmp_dir
    test "with --early-stop set but no failures", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "-p",
          "package_a",
          "-p",
          "package_b",
          "-t",
          "cmd",
          "--early-stop",
          "--workspace-path",
          tmp_dir,
          "--",
          "exit",
          "0"
        ]

        captured = capture_io(fn -> RunTask.run(args) end)

        assert_cli_output_match(
          captured,
          [
            "==> :package_a - mix cmd exit 0",
            "==> :package_b - mix cmd exit 0"
          ],
          partial: true
        )
      end)
    end

    @tag :tmp_dir
    test "if allow_failure is set a warning is emitted instead", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "-p",
          "package_a",
          "-p",
          "package_b",
          "-t",
          "cmd",
          "--allow-failure",
          "package_a",
          "--allow-failure",
          "package_b",
          "--workspace-path",
          tmp_dir,
          "--",
          "exit",
          "1"
        ]

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(
          captured,
          [
            "==> :package_a - mix cmd exit 1",
            "** (exit) 1",
            ":package_a mix cmd exit 1 failed with 1",
            "==> :package_b - mix cmd exit 1",
            "** (exit) 1",
            ":package_b mix cmd exit 1 failed with 1",
            "WARNING task failed in 2 projects but the --alow-failure flag is set",
            "failed projects - [:package_a, :package_b]"
          ],
          partial: true
        )
      end)
    end
  end

  describe "partitioned runs" do
    @tag :tmp_dir
    test "no-op if --partitions is set to 1", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "--workspace-path",
          tmp_dir,
          "--partitions",
          "1",
          "--dry-run" | @default_run_task
        ]

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(captured, [
          "Running task in 11 workspace projects",
          "==> :package_a - mix format --check-formatted mix.exs",
          "==> :package_b - mix format --check-formatted mix.exs",
          "==> :package_c - mix format --check-formatted mix.exs",
          "==> :package_d - mix format --check-formatted mix.exs",
          "==> :package_e - mix format --check-formatted mix.exs",
          "==> :package_f - mix format --check-formatted mix.exs",
          "==> :package_g - mix format --check-formatted mix.exs",
          "==> :package_h - mix format --check-formatted mix.exs",
          "==> :package_i - mix format --check-formatted mix.exs",
          "==> :package_j - mix format --check-formatted mix.exs",
          "==> :package_k - mix format --check-formatted mix.exs"
        ])
      end)
    end

    @tag :tmp_dir
    test "raises if --partitions is set and no env variable is present", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "--workspace-path",
          tmp_dir,
          "--partitions",
          "4",
          "--dry-run" | @default_run_task
        ]

        expected_message =
          "The WORKSPACE_RUN_PARTITION environment variable must be set to " <>
            "an integer between 1..4 when the --partitions option is set, got: nil"

        assert_raise Mix.Error, expected_message, fn -> RunTask.run(args) end
      end)
    end

    @tag :tmp_dir
    test "with --partitions and env variable set", %{tmp_dir: tmp_dir} do
      Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
        args = [
          "--workspace-path",
          tmp_dir,
          "--partitions",
          "4",
          "--dry-run" | @default_run_task
        ]

        # partition 1
        System.put_env("WORKSPACE_RUN_PARTITION", "1")

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(captured, [
          "Running task in 3 workspace projects",
          "==> :package_a - mix format --check-formatted mix.exs",
          "==> :package_e - mix format --check-formatted mix.exs",
          "==> :package_i - mix format --check-formatted mix.exs"
        ])

        # partition 2
        System.put_env("WORKSPACE_RUN_PARTITION", "2")

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(captured, [
          "Running task in 3 workspace projects",
          "==> :package_b - mix format --check-formatted mix.exs",
          "==> :package_f - mix format --check-formatted mix.exs",
          "==> :package_j - mix format --check-formatted mix.exs"
        ])

        # partition 3
        System.put_env("WORKSPACE_RUN_PARTITION", "3")

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(captured, [
          "Running task in 3 workspace projects",
          "==> :package_c - mix format --check-formatted mix.exs",
          "==> :package_g - mix format --check-formatted mix.exs",
          "==> :package_k - mix format --check-formatted mix.exs"
        ])

        # partition 4
        System.put_env("WORKSPACE_RUN_PARTITION", "4")

        captured =
          capture_io(fn ->
            RunTask.run(args)
          end)

        assert_cli_output_match(captured, [
          "Running task in 2 workspace projects",
          "==> :package_d - mix format --check-formatted mix.exs",
          "==> :package_h - mix format --check-formatted mix.exs"
        ])
      end)
    after
      System.delete_env("WORKSPACE_RUN_PARTITION")
    end
  end

  @tag :tmp_dir
  test "with --export option set", %{tmp_dir: tmp_dir} do
    Workspace.Test.with_workspace(tmp_dir, [], :default, fn ->
      output = Path.join(tmp_dir, "run.json")

      expected = """
      * exported execution results to #{output}
      """

      assert capture_io(fn ->
               RunTask.run([
                 "--workspace-path",
                 tmp_dir,
                 "--task",
                 "cmd",
                 "--export",
                 output,
                 "--dry-run",
                 "--",
                 "exit 0"
               ])
             end) =~ expected

      runs = File.read!(output) |> Jason.decode!()

      for run <- runs do
        assert run["status"] == "skip"
        assert is_nil(run["status_code"])
        assert is_nil(run["output"])
        assert run["duration"] == run["completed_at"] - run["triggered_at"]
      end

      assert length(runs) == 11
    end)
  end

  @tag :tmp_dir
  test "the output of the executed tasks is exported", %{tmp_dir: tmp_dir} do
    Workspace.Test.with_workspace(
      tmp_dir,
      [],
      [{:foo, "packages/foo", []}, {:bar, "packages/bar", []}],
      fn ->
        output_file = Path.join(tmp_dir, "run.json")

        capture_io(fn ->
          RunTask.run([
            "--workspace-path",
            tmp_dir,
            "--task",
            "cmd",
            "--export",
            output_file,
            "--",
            "echo \"Hello\nworld\""
          ])
        end)

        runs = File.read!(output_file) |> Jason.decode!()

        for run <- runs do
          assert run["status"] == "ok"
          assert run["status_code"] == 0
          assert run["output"] == "Hello\nworld\n"
        end

        assert length(runs) == 2
      end
    )
  end
end
