defmodule TarakanWeb.InfestationLiveTest do
  use TarakanWeb.ConnCase, async: true

  import Phoenix.LiveViewTest

  alias Tarakan.FindingMemory

  test "index and show render multi-repo patterns", %{conn: conn} do
    submitter = github_account_fixture()
    other = github_account_fixture()
    repo_a = listed_github_repository_fixture(submitter)
    {:ok, repo_b} = Tarakan.Repositories.register_github_repository("acme/widget", other)
    repo_b = listed_repository_fixture(repo_b)

    title = "Live infestation #{System.unique_integer([:positive])}"

    findings = fn path ->
      Jason.encode!(%{
        "tarakan_scan_format" => 1,
        "findings" => [
          %{
            "file" => path,
            "severity" => "critical",
            "title" => title,
            "description" => "Infestation liveview fixture."
          }
        ]
      })
    end

    scan_fixture(repo_a, submitter, %{"findings_json" => findings.("a.ex")})
    scan_fixture(repo_b, other, %{"findings_json" => findings.("b.ex")})
    # repo_a is hit twice by the same pattern at different paths, so the
    # per-repo rollup and the per-finding ledger must disagree on row count.
    scan_fixture(repo_a, submitter, %{"findings_json" => findings.("a2.ex")})

    pattern = FindingMemory.pattern_key(title)

    {:ok, index, html} = live(conn, ~p"/infestations")
    assert html =~ "Infestations"
    assert has_element?(index, "#infestations-constellation")
    assert has_element?(index, "#infestation-#{pattern}")

    {:ok, show, show_html} = live(conn, ~p"/infestations/#{pattern}")
    assert show_html =~ title
    assert has_element?(show, "#infestation-repo-count", "2")
    assert has_element?(show, "#infestation-graph")
    assert has_element?(show, "#infestation-instances")

    # The repo table is a rollup: one row per repository, carrying the counts
    # that distinguish it from the instance ledger below.
    graph_rows = show |> element("#infestation-graph tbody") |> render()
    assert graph_rows =~ "#{repo_a.owner}/#{repo_a.name}"
    assert graph_rows =~ "#{repo_b.owner}/#{repo_b.name}"

    row_a = show |> element("#infestation-graph-node-#{repo_a.id}") |> render() |> squish()
    assert row_a =~ ">2</td>", "repo_a is hit twice and must report 2, not a blank cell"

    row_b = show |> element("#infestation-graph-node-#{repo_b.id}") |> render() |> squish()
    assert row_b =~ ">1</td>", "repo_b is hit once and must report 1"

    # 2 repository rows, 3 instance rows: the two views are different grains.
    ledger = show |> element("#infestation-ledger") |> render()
    assert length(Regex.scan(~r/<li[^>]+id="ledger_instances-/, ledger)) == 3
  end

  describe "fix propagation" do
    setup %{conn: conn} do
      submitter = github_account_fixture()
      other = github_account_fixture()

      source = listed_github_repository_fixture(submitter)
      {:ok, target} = Tarakan.Repositories.register_github_repository("acme/widget", other)
      target = listed_repository_fixture(target)

      title = "Propagatable finding #{System.unique_integer([:positive])}"

      findings =
        Jason.encode!(%{
          "tarakan_scan_format" => 1,
          "findings" => [
            %{
              "file" => "lib/handler.ex",
              "severity" => "critical",
              "title" => title,
              "description" => "Shared across both repositories."
            }
          ]
        })

      source_scan = scan_fixture(source, submitter, %{"findings_json" => findings})
      scan_fixture(target, other, %{"findings_json" => findings})

      # Settle the source instance as fixed so a template can be captured.
      [occurrence] = source_scan.findings

      source_finding =
        Tarakan.Repo.get!(Tarakan.Scans.CanonicalFinding, occurrence.canonical_finding_id)
        |> Ecto.Changeset.change(%{
          status: "fixed",
          fixed_at: DateTime.utc_now(),
          fixed_commit_sha: source_scan.commit_sha
        })
        |> Tarakan.Repo.update!()

      %Tarakan.Scans.FindingCheck{}
      |> Tarakan.Scans.FindingCheck.changeset(%{
        commit_sha: source_scan.commit_sha,
        verdict: "fixed",
        provenance: "human",
        notes: "Escaped the interpolated value and added a regression test."
      })
      |> Ecto.Changeset.put_change(:canonical_finding_id, source_finding.id)
      |> Ecto.Changeset.put_change(:account_id, submitter.id)
      |> Tarakan.Repo.insert!()

      moderator = moderator_account_fixture()

      {:ok, template} =
        Tarakan.Fixes.capture_fix(
          Tarakan.Accounts.Scope.for_account(moderator),
          source_finding
        )

      %{
        conn: log_in_account(conn, moderator),
        pattern: FindingMemory.pattern_key(title),
        source: source,
        target: target,
        template: template
      }
    end

    test "shows the captured fix and carries it to the remaining repository", %{
      conn: conn,
      pattern: pattern,
      source: source,
      target: target,
      template: template
    } do
      {:ok, view, html} = live(conn, ~p"/infestations/#{pattern}")

      assert html =~ "Fixed on"
      assert html =~ "#{source.owner}/#{source.name}"
      assert html =~ "Escaped the interpolated value"
      assert has_element?(view, "#infestation-propagate-fix", "Propagate to 1")

      html = view |> element("#infestation-propagate-fix") |> render_click()

      assert html =~ "Opened 1 fix job"
      assert html =~ "carried to 1 repo"

      # A real, public job now exists on the other repository, carrying the fix.
      # Scan submission auto-opens a verification job too, so reach the fix job
      # through the propagation rather than by repository alone.
      assert [propagation] = Tarakan.Fixes.list_propagations(template)
      assert propagation.repository_id == target.id

      task = propagation.review_task
      assert task.status == "open"
      assert task.visibility == "public"
      assert task.description =~ "Escaped the interpolated value"
    end

    test "anonymous visitors see the fix but cannot propagate it", %{pattern: pattern} do
      {:ok, view, html} = live(build_conn(), ~p"/infestations/#{pattern}")

      assert html =~ "Fixed on"
      refute has_element?(view, "#infestation-propagate-fix")
    end
  end

  # Collapse the whitespace HEEx leaves around interpolated cell values so
  # assertions can match on the value itself.
  defp squish(html) do
    html
    |> String.replace(~r/>\s+/, ">")
    |> String.replace(~r/\s+</, "<")
  end
end
