defmodule Mix.Tasks.Hex.OutdatedTest do
  use HexTest.IntegrationCase

  defmodule OutdatedDeps.MixProject do
    def project do
      [
        app: :outdated_app,
        version: "0.0.2",
        deps: [
          {:bar, "0.1.0"},
          {:ex_doc, "~> 0.0.1"}
        ]
      ]
    end
  end

  defmodule OutdatedBetaDeps.MixProject do
    def project do
      [
        app: :outdated_app,
        version: "0.0.1",
        deps: [
          {:beta, ">= 0.0.0"}
        ]
      ]
    end
  end

  defmodule OutdatedApp.MixProject do
    def project do
      [
        app: :outdated_app,
        version: "0.0.1",
        deps: [
          {:ex_doc, ">= 0.0.0"},
          {:postgrex, "0.2.0"},
          {:ecto, "0.2.0"}
        ]
      ]
    end
  end

  defmodule NotOutdatedApp.MixProject do
    def project do
      [
        app: :outdated_app,
        version: "0.0.1",
        deps: [
          {:ex_doc, ">= 0.0.0"}
        ]
      ]
    end
  end

  defmodule UpdateNotPossibleApp.MixProject do
    def project do
      [
        app: :outdated_app,
        version: "0.0.1",
        deps: [
          {:baz, "0.1.0"},
          {:bar, "0.1.0"}
        ]
      ]
    end
  end

  defmodule WithoutHexDeps.MixProject do
    def project do
      [
        app: :outdated_app,
        version: "0.0.1",
        deps: []
      ]
    end
  end

  defmodule OutdatedMultiDeps.MixProject do
    def project do
      [
        app: :outdated_app,
        version: "0.0.2",
        deps: [
          {:baz, "0.1.0"},
          {:bar, "0.1.0"}
        ]
      ]
    end
  end

  defmodule OutdatedDepsWithTypes.MixProject do
    def project do
      [
        app: :outdated_app,
        version: "0.0.2",
        deps: [
          {:ex_doc, "0.0.1"},
          {:beta, "1.0.0", only: :dev},
          {:tired, "0.1.0", only: :test},
          {:foo, "0.1.0", only: [:dev, :test]}
        ]
      ]
    end
  end

  test "outdated" do
    Mix.Project.push(OutdatedDeps.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{bar: {:hex, :bar, "0.1.0"}, foo: {:hex, :foo, "0.1.0"}})

      Mix.Task.run("deps.get")
      flush()

      assert catch_throw(Mix.Task.run("hex.outdated")) == {:exit_code, 1}

      bar =
        [
          [:bright, "bar", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :green, "0.1.0", :reset],
          ["   ", :green, "Up-to-date", :reset],
          "           "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^bar]}
      refute_received {:mix_shell, :info, ["foo" <> _]}
    end)
  end

  test "outdated --all" do
    Mix.Project.push(OutdatedDeps.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{bar: {:hex, :bar, "0.1.0"}, foo: {:hex, :foo, "0.1.0"}})

      Mix.Task.run("deps.get")
      flush()

      assert catch_throw(Mix.Task.run("hex.outdated", ["--all"])) == {:exit_code, 1}

      bar =
        [
          [:bright, "bar", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :green, "0.1.0", :reset],
          ["   ", :green, "Up-to-date", :reset],
          "           "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      foo =
        [
          [:bright, "foo", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :red, "0.1.1", :reset],
          ["   ", :yellow, "Update possible", :reset],
          "      "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      ex_doc =
        [
          [:bright, "ex_doc", :reset],
          ["      ", :reset],
          ["      ", "0.0.1", :reset],
          ["    ", :red, "0.1.0", :reset],
          ["   ", :red, "Update not possible", :reset],
          "  "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^bar]}
      assert_received {:mix_shell, :info, [^foo]}
      assert_received {:mix_shell, :info, [^ex_doc]}
    end)
  end

  test "outdated --all --sort status" do
    Mix.Project.push(OutdatedApp.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{bar: {:hex, :bar, "0.1.0"}, foo: {:hex, :foo, "0.1.0"}})

      Mix.Task.run("deps.get")
      flush()

      assert catch_throw(Mix.Task.run("hex.outdated", ["--all", "--sort", "status"])) ==
               {:exit_code, 1}

      _bar =
        [
          [:bright, "bar", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :green, "0.1.0", :reset],
          ["   ", :green, "Up-to-date", :reset],
          "           "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      _foo =
        [
          [:bright, "foo", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :red, "0.1.1", :reset],
          ["   ", :yellow, "Update possible", :reset],
          "      "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      _ex_doc =
        [
          [:bright, "ex_doc", :reset],
          ["      ", :reset],
          ["      ", "0.0.1", :reset],
          ["    ", :red, "0.1.0", :reset],
          ["   ", :red, "Update not possible", :reset],
          "  "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      lines = flush()
      extracted_statuses = extract_statuses(lines)

      assert extracted_statuses == [
               "Up-to-date",
               "Update not possible",
               "Update not possible",
               "Update not possible",
               "Update possible"
             ]
    end)
  end

  test "outdated --all with multiple dependent packages" do
    Mix.Project.push(OutdatedMultiDeps.MixProject)

    in_tmp(fn ->
      set_home_tmp()

      Mix.Dep.Lock.write(%{
        foo: {:hex, :foo, "0.1.0"},
        bar: {:hex, :bar, "0.1.0"},
        baz: {:hex, :baz, "0.1.0"}
      })

      Mix.Task.run("deps.get")
      flush()

      assert catch_throw(Mix.Task.run("hex.outdated", ["--all"])) == {:exit_code, 1}

      foo =
        [
          [:bright, "foo", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :red, "0.1.1", :reset],
          ["   ", :red, "Update not possible", :reset],
          "  "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^foo]}
    end)
  end

  test "outdated --all --within-requirements" do
    Mix.Project.push(OutdatedDeps.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{bar: {:hex, :bar, "0.1.0"}, foo: {:hex, :foo, "0.1.0"}})

      Mix.Task.run("deps.get")
      flush()

      assert catch_throw(Mix.Task.run("hex.outdated", ["--all", "--within-requirements"])) ==
               {:exit_code, 1}

      bar =
        [
          [:bright, "bar", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :green, "0.1.0", :reset],
          ["   ", :green, "Up-to-date", :reset],
          "           "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      foo =
        [
          [:bright, "foo", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :red, "0.1.1", :reset],
          ["   ", :yellow, "Update possible", :reset],
          "      "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      ex_doc =
        [
          [:bright, "ex_doc", :reset],
          ["      ", :reset],
          ["      ", "0.0.1", :reset],
          ["    ", :red, "0.1.0", :reset],
          ["   ", :red, "Update not possible", :reset],
          "  "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^bar]}
      assert_received {:mix_shell, :info, [^foo]}
      assert_received {:mix_shell, :info, [^ex_doc]}
    end)
  end

  test "outdated --all --within-requirements (update not possible)" do
    Mix.Project.push(UpdateNotPossibleApp.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{foo: {:hex, :foo, "0.1.0"}})

      Mix.Task.run("deps.get")
      flush()

      assert Mix.Task.run("hex.outdated", ["--all", "--within-requirements"]) ==
               nil

      bar =
        [
          [:bright, "bar", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :green, "0.1.0", :reset],
          ["   ", :green, "Up-to-date", :reset],
          "           "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      baz =
        [
          [:bright, "baz", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :green, "0.1.0", :reset],
          ["   ", :green, "Up-to-date", :reset],
          "           "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      foo =
        [
          [:bright, "foo", :reset],
          ["         ", :reset],
          ["      ", "0.1.0", :reset],
          ["    ", :red, "0.1.1", :reset],
          ["   ", :red, "Update not possible", :reset],
          "  "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^bar]}
      assert_received {:mix_shell, :info, [^baz]}
      assert_received {:mix_shell, :info, [^foo]}
    end)
  end

  test "outdated --all --within-requirements (not outdated)" do
    Mix.Project.push(NotOutdatedApp.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{ex_doc: {:hex, :ex_doc, "0.1.0"}})

      Mix.Task.run("deps.get")
      flush()

      assert Mix.Task.run("hex.outdated", ["ex_doc"]) == nil

      msg =
        ["Current version ", :bright, "0.1.0", :reset, " of dependency is up to date!"]
        |> IO.ANSI.format_fragment()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^msg]}
    end)
  end

  test "outdated --pre" do
    Mix.Project.push(OutdatedBetaDeps.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{beta: {:hex, :beta, "1.0.0"}})

      Mix.Task.run("deps.get")
      flush()

      Mix.Task.run("hex.outdated", [])

      beta =
        [
          [:bright, "beta", :reset],
          ["        ", :reset],
          ["      ", "1.0.0", :reset],
          ["    ", :green, "1.0.0", :reset],
          ["   ", :green, "Up-to-date", :reset],
          ["  "]
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^beta]}

      Mix.Task.reenable("hex.outdated")
      assert catch_throw(Mix.Task.run("hex.outdated", ["--pre"])) == {:exit_code, 1}

      beta =
        [
          [:bright, "beta", :reset],
          ["        ", :reset],
          ["      ", "1.0.0", :reset],
          ["    ", :red, "1.1.0-beta", :reset],
          ["  ", :yellow, "Update possible", :reset],
          "  "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^beta]}
    end)
  end

  test "outdated app" do
    Mix.Project.push(OutdatedApp.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{ex_doc: {:hex, :ex_doc, "0.0.1"}})

      Mix.Task.run("deps.get")
      flush()

      assert catch_throw(Mix.Task.run("hex.outdated", ["ex_doc"])) == {:exit_code, 1}

      msg =
        [
          "There is newer version of the dependency available ",
          [:bright, "0.1.0 > 0.0.1", :reset, "!"]
        ]
        |> IO.ANSI.format_fragment()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^msg]}

      mix =
        [
          :bright,
          "mix.exs",
          :reset,
          "   ",
          :green,
          ">= 0.0.0",
          :reset,
          "     ",
          :green,
          "Yes",
          :reset,
          "         "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^mix]}

      ecto =
        [
          :bright,
          "ecto",
          :reset,
          "      ",
          :red,
          "~> 0.0.1",
          :reset,
          "     ",
          :red,
          "No",
          :reset,
          "          "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^ecto]}

      postgrex =
        [
          :bright,
          "postgrex",
          :reset,
          "  ",
          :red,
          "0.0.1",
          :reset,
          "        ",
          :red,
          "No",
          :reset,
          "          "
        ]
        |> IO.ANSI.format()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^postgrex]}
    end)
  end

  test "not outdated app" do
    Mix.Project.push(NotOutdatedApp.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{ex_doc: {:hex, :ex_doc, "0.1.0"}})

      Mix.Task.run("deps.get")
      flush()

      Mix.Task.run("hex.outdated", ["ex_doc"])

      msg =
        ["Current version ", :bright, "0.1.0", :reset, " of dependency is up to date!"]
        |> IO.ANSI.format_fragment()
        |> List.to_string()

      assert_received {:mix_shell, :info, [^msg]}
    end)
  end

  test "without hex deps" do
    Mix.Project.push(WithoutHexDeps.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{})

      Mix.Task.run("deps.get")
      flush()

      Mix.Task.run("hex.outdated")
      msg = "No hex dependencies"
      assert_received {:mix_shell, :info, [^msg]}
    end)
  end

  test "umbrella projects" do
    in_tmp("umbrella", fn ->
      File.write!("mix.exs", """
      defmodule Umbrella.MixProject do
        use Mix.Project

        def project do
          [apps_path: "apps",
           version: "0.0.1",
           deps: [{:ex_doc, "~> 0.0.1"}]]
        end
      end
      """)

      Mix.Project.in_project(:umbrella, ".", fn _ ->
        File.mkdir_p!("apps/bacon")

        File.write!("apps/bacon/mix.exs", """
        defmodule Bacon.MixProject do
          use Mix.Project

          def project do
            [app: :bacon,
             version: "0.1.0",
             build_path: "../../_build",
             config_path: "../../config/config.exs",
             deps_path: "../../deps",
             lockfile: "../../mix.lock",
             deps: [{:bar, "0.1.0"}]]
          end
        end
        """)

        Mix.Project.in_project(:bacon, "apps/bacon", fn _ ->
          Mix.Task.run("deps.get")
          flush()
        end)

        Mix.Task.run("deps.get")
        flush()

        ex_doc =
          [
            [:bright, "ex_doc", :reset],
            ["      ", :reset],
            ["      ", "0.0.1", :reset],
            ["    ", :red, "0.1.0", :reset],
            ["   ", :red, "Update not possible", :reset],
            "  "
          ]
          |> IO.ANSI.format()
          |> List.to_string()

        bar =
          [
            [:bright, "bar", :reset],
            ["         ", :reset],
            ["      ", "0.1.0", :reset],
            ["    ", :green, "0.1.0", :reset],
            ["   ", :green, "Up-to-date", :reset],
            "           "
          ]
          |> IO.ANSI.format()
          |> List.to_string()

        foo =
          [
            [:bright, "foo", :reset],
            ["         ", :reset],
            ["      ", "0.1.1", :reset],
            ["    ", :green, "0.1.1", :reset],
            ["   ", :green, "Up-to-date", :reset],
            "           "
          ]
          |> IO.ANSI.format()
          |> List.to_string()

        assert catch_throw(Mix.Task.run("hex.outdated")) == {:exit_code, 1}
        assert_received {:mix_shell, :info, [^ex_doc]}
        assert_received {:mix_shell, :info, [^bar]}
        refute_received {:mix_shell, :info, [^foo]}

        assert catch_throw(Mix.Tasks.Hex.Outdated.run(["--all"])) == {:exit_code, 1}
        assert_received {:mix_shell, :info, [^ex_doc]}
        assert_received {:mix_shell, :info, [^bar]}
        assert_received {:mix_shell, :info, [^foo]}
      end)
    end)
  end

  test "outdated shows a generic diff command hint when updates are available" do
    Mix.Project.push(OutdatedDeps.MixProject)

    in_tmp(fn ->
      set_home_tmp()
      Mix.Dep.Lock.write(%{bar: {:hex, :bar, "0.1.0"}, foo: {:hex, :foo, "0.1.0"}})

      Mix.Task.run("deps.get")
      flush()

      assert catch_throw(Mix.Task.run("hex.outdated", ["--all"])) == {:exit_code, 1}

      lines = flush()
      output = Enum.map_join(lines, "\n", fn {_, _, [line]} -> line end)

      assert output =~
               "To view the diff of a specific update, run `mix hex.package diff APP FROM..TO`."
    end)
  end

  defp extract_statuses(lines) do
    Enum.flat_map(lines, fn {_, _, [line]} ->
      ~r/Up-to-date|Update not possible|Update possible/
      |> Regex.scan(line)
      |> List.flatten()
    end)
  end

  test "outdated with dependency types displays only column" do
    Mix.Project.push(OutdatedDepsWithTypes.MixProject)

    in_tmp(fn ->
      set_home_tmp()

      Mix.Dep.Lock.write(%{
        ex_doc: {:hex, :ex_doc, "0.0.1"},
        beta: {:hex, :beta, "1.0.0"},
        tired: {:hex, :tired, "0.1.0"},
        foo: {:hex, :foo, "0.1.0"}
      })

      Mix.Task.run("deps.get")
      flush()

      catch_throw(Mix.Task.run("hex.outdated"))

      # Check that the Only column is present in the header
      assert_received {:mix_shell, :info, [header_line]}
      assert header_line =~ "Dependency"
      assert header_line =~ "Only"
      assert header_line =~ "Current"
      assert header_line =~ "Latest"
      assert header_line =~ "Status"

      # Check that dependencies show their correct only values
      # Note: dependencies without :only show empty string in the Only column
      output_lines =
        Enum.map(1..10, fn _ ->
          receive do
            {:mix_shell, :info, [line]} -> line
          after
            100 -> nil
          end
        end)
        |> Enum.filter(&(&1 != nil))

      combined_output = Enum.join(output_lines, "\n")
      assert combined_output =~ "foo" and combined_output =~ "dev,test"
      assert combined_output =~ "beta" and combined_output =~ "dev"
      assert combined_output =~ "tired" and combined_output =~ "test"
    end)
  end

  test "outdated --only dev filters to only dev dependencies" do
    Mix.Project.push(OutdatedDepsWithTypes.MixProject)

    in_tmp(fn ->
      set_home_tmp()

      Mix.Dep.Lock.write(%{
        ex_doc: {:hex, :ex_doc, "0.0.1"},
        beta: {:hex, :beta, "1.0.0"},
        tired: {:hex, :tired, "0.1.0"},
        foo: {:hex, :foo, "0.1.0"}
      })

      Mix.Task.run("deps.get")
      flush()

      catch_throw(Mix.Task.run("hex.outdated", ["--only", "dev"]))

      # Should show only dependencies with :only set to :dev
      # Since we filtered to only "dev", the output should contain only ex_doc
      output_lines =
        Enum.map(1..10, fn _ ->
          receive do
            {:mix_shell, :info, [line]} -> line
          after
            100 -> nil
          end
        end)
        |> Enum.filter(&(&1 != nil))

      combined_output = Enum.join(output_lines, "\n")
      assert combined_output =~ "beta"
      # foo has [:dev, :test] not just :dev, but beta only has :dev so both should show
      assert combined_output =~ "foo"
      refute combined_output =~ "ex_doc"
    end)
  end

  test "outdated --only dev,test filters to dev and test dependencies" do
    Mix.Project.push(OutdatedDepsWithTypes.MixProject)

    in_tmp(fn ->
      set_home_tmp()

      Mix.Dep.Lock.write(%{
        ex_doc: {:hex, :ex_doc, "0.0.1"},
        beta: {:hex, :beta, "1.0.0"},
        tired: {:hex, :tired, "0.1.0"},
        foo: {:hex, :foo, "0.1.0"}
      })

      Mix.Task.run("deps.get")
      flush()

      catch_throw(Mix.Task.run("hex.outdated", ["--only", "dev,test"]))

      # Should show dependencies with :only set to :dev or :test
      # This should include ex_doc (dev) and plug (test)
      # bypass has [:dev, :test] which displays as "dev,test" but should match both "dev" and "test" individually
      output_lines =
        Enum.map(1..10, fn _ ->
          receive do
            {:mix_shell, :info, [line]} -> line
          after
            100 -> nil
          end
        end)
        |> Enum.filter(&(&1 != nil))

      combined_output = Enum.join(output_lines, "\n")
      assert combined_output =~ "tired"
      assert combined_output =~ "beta"
      # foo should be included since it has dev and test
      assert combined_output =~ "foo"
      refute combined_output =~ "ex_doc"
    end)
  end
end
