defmodule LinuxRolloutTest do
  use ExUnit.Case, async: false

  import ExUnit.CaptureIO

  alias LinuxRollout.{
    Auth,
    CLI,
    Doctor,
    GitHubRelease,
    LocalConfig,
    ReleasePrep,
    Renderer,
    State,
    Submitter,
    Validator,
    Workspace
  }

  alias LinuxRollout.Adapters.{
    BugzillaApi,
    ConditionalGitSubmit,
    GitSubmit,
    LaunchpadApi,
    MailSubmit,
    ServiceCli
  }

  alias LinuxRollout.Util

  setup do
    root =
      Path.join(System.tmp_dir!(), "linux_rollout_test_#{System.unique_integer([:positive])}")

    File.rm_rf!(root)
    File.mkdir_p!(root)
    File.mkdir_p!(Path.join(root, "home"))

    previous_env =
      for key <- [
            "HOME",
            "LINUX_ROLLOUT_SHARE_DIR",
            "LINUX_ROLLOUT_CACHE_DIR",
            "LINUX_ROLLOUT_CONFIG",
            "PATH"
          ],
          into: %{} do
        {key, System.get_env(key)}
      end

    System.put_env("HOME", Path.join(root, "home"))
    System.put_env("LINUX_ROLLOUT_SHARE_DIR", Path.join(root, ".local-share"))
    System.put_env("LINUX_ROLLOUT_CACHE_DIR", Path.join(root, ".local-cache"))
    System.put_env("LINUX_ROLLOUT_CONFIG", Path.join(root, ".local-share/config.yaml"))
    write_fixture_workspace(root)

    on_exit(fn ->
      Enum.each(previous_env, fn
        {key, nil} -> System.delete_env(key)
        {key, value} -> System.put_env(key, value)
      end)

      File.rm_rf!(root)
    end)

    {:ok, root: root}
  end

  test "workspace loading derives artifact hashes and renders templates", %{root: root} do
    workspace = Workspace.load!(root)
    assert workspace.tokens["source_tarball_sha256"] == String.duplicate("a", 64)
    assert workspace.tokens["wire_share_jar_sha256"] == String.duplicate("b", 64)
    assert String.length(workspace.tokens["source_tarball_nix_base32"]) > 10
    assert String.length(workspace.tokens["source_tarball_sha512"]) == 128

    rendered_root = Renderer.render!(workspace)
    rendered_manifest = Path.join(rendered_root, "flathub/cx.hermes.WireShare.yaml")
    rendered_formula = Path.join(rendered_root, "homebrew/wireshare.rb")
    rendered_openbsd = Path.join(rendered_root, "openbsd/distinfo")
    rendered_alpine = Path.join(rendered_root, "alpine/APKBUILD")

    assert File.read!(rendered_manifest) =~ "id: cx.hermes.WireShare"
    assert File.read!(rendered_manifest) =~ String.duplicate("a", 64)
    assert File.read!(rendered_formula) =~ "sha256 \"#{String.duplicate("a", 64)}\""

    assert File.read!(rendered_openbsd) =~
             "SHA256 (WireShare-9.9-source.tar.gz) = #{String.duplicate("a", 64)}"

    assert File.read!(rendered_alpine) =~
             "sha256sums=\"#{String.duplicate("a", 64)}  $pkgname-$pkgver.tar.gz\""
  end

  test "workspace loading applies local config overrides for accounts and destinations", %{
    root: root
  } do
    LocalConfig.set!("accounts.launchpad_owner", "local-launchpad")
    LocalConfig.set!("targets.homebrew.destination.fork_repo", "local/homebrew-wireshare")

    workspace = Workspace.load!(root)

    assert workspace.tokens["launchpad_owner"] == "local-launchpad"

    assert get_in(workspace.targets, ["homebrew", "destination", "fork_repo"]) ==
             "local/homebrew-wireshare"
  end

  test "workspace auto-detects obs and pkgsrc identities from local auth state", %{root: root} do
    write_file(root, "home/.oscrc", "[general]\nuser = obs-auto\n")

    fake_bin = Path.join(root, "fake-bin")

    write_file(root, "fake-bin/ssh", """
    #!/bin/sh
    if [ "$1" = "-G" ]; then
      echo "user pkgsrc-auto"
      exit 0
    fi
    exit 1
    """)

    File.chmod!(Path.join(fake_bin, "ssh"), 0o755)
    System.put_env("PATH", "#{fake_bin}:#{System.get_env("PATH")}")

    workspace = Workspace.load!(root)

    assert workspace.tokens["obs_owner"] == "obs-auto"
    assert workspace.tokens["obs_project"] == "home:obs-auto:wireshare"
    assert workspace.tokens["pkgsrc_wip_user"] == "pkgsrc-auto"

    assert get_in(workspace.targets, ["pkgsrc", "destination", "repo"]) ==
             "pkgsrc-auto@wip.pkgsrc.org:/pkgsrc-wip.git"

    assert Doctor.inspect_target(workspace, Workspace.target!(workspace, "pkgsrc")).status == "ok"
  end

  test "human gate requires approval before resume can advance", %{root: root} do
    workspace = Workspace.load!(root)
    ReleasePrep.run!(workspace, skip_gradle: true, dry_run: true)
    Validator.run!(workspace)

    Submitter.submit!(workspace, ["flathub"], dry_run: true, no_open: true)
    assert get_in(State.load(workspace), ["targets", "flathub", "status"]) == "awaiting_approval"

    output =
      capture_io(fn ->
        Submitter.submit!(
          workspace,
          ["flathub"],
          dry_run: true,
          no_open: true,
          acknowledge: true
        )
      end)

    assert get_in(State.load(workspace), ["targets", "flathub", "status"]) == "approved"

    next_steps =
      Path.join(workspace.bundle_root, "flathub/APPROVED_NEXT_STEPS.md")
      |> File.read!()

    pr_body =
      Path.join(workspace.bundle_root, "flathub/PR_BODY.md")
      |> File.read!()

    assert output =~ "gh pr create"
    assert output =~ "--base new-pr"
    assert output =~ "--head tester:cx-hermes-WireShare-submission"
    refute next_steps =~ ~r/^gh pr create/m
    refute next_steps =~ "/Users/"
    refute next_steps =~ "tester:"
    assert pr_body =~ "Submitting cx.hermes.WireShare to Flathub."
  end

  test "cli smoke commands include auth and status output", %{root: root} do
    output =
      capture_io(fn ->
        CLI.main(["prepare", "--cwd", root, "--skip-gradle", "--dry-run"])
        CLI.main(["validate", "--cwd", root])
        CLI.main(["doctor", "--cwd", root, "--target", "homebrew"])
        CLI.main(["auth", "status", "--cwd", root, "--target", "guix"])
        CLI.main(["render", "--cwd", root])
        CLI.main(["status", "--cwd", root, "--target", "flathub"])
      end)

    assert output =~ "[linux-rollout] prepare: starting release prep"
    assert output =~ "Prepared Linux release inputs"
    assert output =~ "Validated rendered rollout bundle"
    assert output =~ "Doctor inspected"
    assert output =~ "guix:"
    assert output =~ "last=pending"
  end

  test "config set, status, and unset manage local overrides through the CLI", %{root: root} do
    output =
      capture_io(fn ->
        CLI.main(["config", "set", "--cwd", root, "accounts.launchpad_owner", "local-owner"])
        CLI.main(["config", "status", "--cwd", root])
        CLI.main(["config", "unset", "--cwd", root, "accounts.launchpad_owner"])
      end)

    assert output =~ "Updated local config:"
    assert output =~ "Local config:"
    assert output =~ "accounts.launchpad_owner: local-owner"
    assert output =~ "Unset accounts.launchpad_owner"
    assert LocalConfig.load() == %{}
  end

  test "launchpad auth login stores a local owner override", %{root: root} do
    write_file(root, ".local-share/launchpad/bin/python", "#!/bin/sh\nexec python3 \"$@\"\n")
    File.chmod!(Path.join(root, ".local-share/launchpad/bin/python"), 0o755)

    workspace = Workspace.load!(root)

    Auth.login!(workspace, ["snap"])

    assert get_in(LocalConfig.load(), ["accounts", "launchpad_owner"]) == "launchpad-tester"
  end

  test "obs auth login stores local owner and derived project overrides", %{root: root} do
    fake_bin = Path.join(root, "fake-bin")

    write_file(root, "fake-bin/osc", """
    #!/bin/sh
    cat > "$HOME/.oscrc" <<'EOF'
    [general]
    user = obs-tester
    EOF
    echo "obs auth ok"
    """)

    File.chmod!(Path.join(fake_bin, "osc"), 0o755)
    System.put_env("PATH", "#{fake_bin}:#{System.get_env("PATH")}")

    workspace = Workspace.load!(root)
    Auth.login!(workspace, ["obs"])

    assert get_in(LocalConfig.load(), ["accounts", "obs_owner"]) == "obs-tester"
    assert get_in(LocalConfig.load(), ["accounts", "obs_project"]) == "home:obs-tester:wireshare"
  end

  test "bugzilla auth login runs interactively for Mageia", %{root: root} do
    fake_bin = Path.join(root, "fake-bin")

    write_file(root, "fake-bin/curl", """
    #!/bin/sh
    printf '%s\\n' '{"users":[{"name":"mageia-tester","real_name":"Mageia Tester"}]}'
    """)

    File.chmod!(Path.join(fake_bin, "curl"), 0o755)
    System.put_env("PATH", "#{fake_bin}:#{System.get_env("PATH")}")

    workspace = Workspace.load!(root)

    output =
      capture_io("mageia-tester\nmageia-api-key\n", fn ->
        Auth.login!(workspace, ["mageia"])
      end)

    assert output =~ "Bugzilla login/email:"
    assert output =~ "Bugzilla API key (input visible in this terminal):"
    assert output =~ "Authenticated to https://bugs.mageia.org as Mageia Tester"
    assert File.exists?(Util.bugzilla_credentials_file("https://bugs.mageia.org"))
  end

  test "git submit dry run supports push-only, GitHub PR, and GitLab MR targets", %{root: root} do
    workspace = Workspace.load!(root)
    Validator.run!(workspace)
    aur_bundle = LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "aur"))

    homebrew_bundle =
      LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "homebrew"))

    alpine_bundle =
      LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "alpine"))

    aur_result =
      GitSubmit.submit(workspace, aur_bundle, Workspace.target!(workspace, "aur"), dry_run: true)

    homebrew_result =
      GitSubmit.submit(workspace, homebrew_bundle, Workspace.target!(workspace, "homebrew"),
        dry_run: true
      )

    alpine_result =
      GitSubmit.submit(workspace, alpine_bundle, Workspace.target!(workspace, "alpine"),
        dry_run: true
      )

    assert aur_result.status == "dry_run"
    assert length(aur_result.units) == 2
    assert homebrew_result.status == "dry_run"
    assert alpine_result.status == "dry_run"
  end

  test "service CLI dry run supports OBS and Copr targets", %{root: root} do
    workspace = Workspace.load!(root)
    Validator.run!(workspace)

    obs_bundle = LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "obs"))
    copr_bundle = LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "copr"))

    obs_result =
      ServiceCli.submit(workspace, obs_bundle, Workspace.target!(workspace, "obs"), dry_run: true)

    copr_result =
      ServiceCli.submit(workspace, copr_bundle, Workspace.target!(workspace, "copr"),
        dry_run: true
      )

    assert obs_result.status == "dry_run"
    assert copr_result.status == "dry_run"
  end

  test "gitlab duplicate MR detection tolerates wrapped CLI output" do
    output = """
    Creating draft merge request for linux-rollout-7.0-alpine into master in alpine/aports

       ERROR

      Post https://gitlab.alpinelinux.org/api/v4/projects/nmatavka%2Faports/merge_requests: 409 {message: [Another open
      merge request already exists for this source branch: !102732]}.
    """

    assert GitSubmit.detect_existing_gitlab_mr(output, "gitlab.alpinelinux.org", "alpine/aports") ==
             %{
               merge_request:
                 "https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/102732"
             }
  end

  test "copr build submission parser treats accepted build handoff as success" do
    output = """
    Build was added to wireshare:
      https://copr.fedorainfracloud.org/coprs/build/10490349
    Created builds: 10490349
    Watching build(s): (this may be safely interrupted)
      18:32:26 Build 10490349: pending
    """

    assert ServiceCli.interpret_copr_build_submission(output) == %{
             accepted: true,
             build_url: "https://copr.fedorainfracloud.org/coprs/build/10490349",
             build_ids: ["10490349"]
           }
  end

  test "github release helper encodes slash tags and finds missing assets" do
    release = %{
      "assets" => [
        %{
          "name" => "WireShare-9.9-source.tar.gz",
          "browser_download_url" =>
            "https://github.com/example/project/releases/download/release/9.9/WireShare-9.9-source.tar.gz"
        },
        %{
          "name" => "SHA256SUMS",
          "browser_download_url" =>
            "https://github.com/example/project/releases/download/release/9.9/SHA256SUMS"
        }
      ]
    }

    expected = ["WireShare-9.9-source.tar.gz", "WireShare.jar", "SHA256SUMS"]

    assert GitHubRelease.api_tag_path("release/9.9") == "release%2F9.9"
    assert GitHubRelease.missing_release_asset_names(release, expected) == ["WireShare.jar"]

    assert GitHubRelease.release_asset_urls(release, expected) == %{
             "WireShare-9.9-source.tar.gz" =>
               "https://github.com/example/project/releases/download/release/9.9/WireShare-9.9-source.tar.gz",
             "SHA256SUMS" =>
               "https://github.com/example/project/releases/download/release/9.9/SHA256SUMS"
           }
  end

  test "launchpad API dry run supports snap and ppa payload generation", %{root: root} do
    workspace = Workspace.load!(root)
    Validator.run!(workspace)

    snap_bundle = LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "snap"))
    ppa_bundle = LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "ppa"))

    snap_result =
      LaunchpadApi.submit(workspace, snap_bundle, Workspace.target!(workspace, "snap"),
        dry_run: true
      )

    ppa_result =
      LaunchpadApi.submit(workspace, ppa_bundle, Workspace.target!(workspace, "ppa"),
        dry_run: true
      )

    assert snap_result.status == "dry_run"
    assert ppa_result.status == "dry_run"
    assert File.exists?(snap_result.payload)
    assert File.exists?(ppa_result.payload)
  end

  test "bugzilla API dry run supports Mageia requests and FreeBSD patches", %{root: root} do
    workspace = Workspace.load!(root)
    Validator.run!(workspace)

    mageia_bundle =
      LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "mageia"))

    freebsd_bundle =
      LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "freebsd"))

    mageia_result =
      BugzillaApi.submit(workspace, mageia_bundle, Workspace.target!(workspace, "mageia"),
        dry_run: true
      )

    freebsd_result =
      BugzillaApi.submit(workspace, freebsd_bundle, Workspace.target!(workspace, "freebsd"),
        dry_run: true
      )

    assert mageia_result.status == "dry_run"
    assert freebsd_result.status == "dry_run"
    assert File.exists?(mageia_result.payload)
    assert File.exists?(freebsd_result.payload)
  end

  test "git submit dry run supports HaikuPorts GitHub PR payloads", %{root: root} do
    workspace = Workspace.load!(root)
    Validator.run!(workspace)
    bundle = LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "haiku"))

    result =
      GitSubmit.submit(workspace, bundle, Workspace.target!(workspace, "haiku"), dry_run: true)

    assert result.status == "dry_run"
    assert File.exists?(Path.join(bundle.source_root, "packaging/haiku/wireshare.recipe"))
  end

  test "mail submit dry run supports Guix intro and threaded resume", %{root: root} do
    workspace = Workspace.load!(root)
    Validator.run!(workspace)
    guix_bundle = LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "guix"))

    intro_result =
      MailSubmit.submit(workspace, guix_bundle, Workspace.target!(workspace, "guix"),
        dry_run: true
      )

    resume_result =
      MailSubmit.submit(
        workspace,
        guix_bundle,
        Workspace.target!(workspace, "guix"),
        dry_run: true,
        thread_address: "12345@debbugs.gnu.org"
      )

    assert intro_result.status == "dry_run"
    assert resume_result.status == "dry_run"
    assert File.exists?(intro_result.preview)
    assert File.dir?(intro_result.patches)
  end

  test "mail submit can send Guix patch replies without git send-email", %{root: root} do
    fake_bin = Path.join(root, "fake-bin")
    real_git = System.find_executable("git") || raise "git is required for this test"

    write_file(root, "fake-bin/git", """
    #!/bin/sh
    if [ "$1" = "send-email" ]; then
      echo "git: 'send-email' is not a git command." >&2
      exit 1
    fi
    exec #{real_git} "$@"
    """)

    File.chmod!(Path.join(fake_bin, "git"), 0o755)
    System.put_env("PATH", "#{fake_bin}:#{System.get_env("PATH")}")

    workspace = Workspace.load!(root)
    Validator.run!(workspace)
    bundle = LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "guix"))

    result =
      MailSubmit.submit(workspace, bundle, Workspace.target!(workspace, "guix"),
        dry_run: true,
        thread_address: "12345@debbugs.gnu.org"
      )

    patch_previews = Path.wildcard(Path.join(result.patches, "*.patch.eml"))
    patch_payloads = Path.wildcard(Path.join(result.patches, "*.patch.mail.json"))

    assert result.status == "dry_run"
    assert patch_previews != []
    assert patch_payloads != []
  end

  test "conditional git submit falls back to manual packet when gated access is absent", %{
    root: root
  } do
    workspace = Workspace.load!(root)
    Validator.run!(workspace)
    bundle = LinuxRollout.Bundles.create!(workspace, Workspace.target!(workspace, "pkgsrc"))
    preflight = Doctor.inspect_target(workspace, Workspace.target!(workspace, "pkgsrc"))

    result =
      ConditionalGitSubmit.submit(
        workspace,
        bundle,
        Workspace.target!(workspace, "pkgsrc"),
        preflight,
        dry_run: true
      )

    assert result.status == "dry_run"
    assert File.exists?(result.handoff)
  end

  test "doctor distinguishes config, cli, and auth blockers" do
    config_target = %{"required_config" => ["destination.repo"], "destination" => %{}}
    cli_target = %{"required_cli" => ["command-that-does-not-exist-123"]}

    auth_target = %{
      "auth_probe" => %{
        "type" => "file",
        "path" => "/definitely/missing/auth/file",
        "help" => "log in first"
      }
    }

    launchpad_auth_target = %{"auth_probe" => %{"type" => "launchpad_auth"}}

    bugzilla_auth_target = %{
      "auth_probe" => %{"type" => "bugzilla_auth", "url" => "https://bugs.example.invalid"}
    }

    assert Doctor.inspect_target(nil, config_target).status == "blocked_missing_config"
    assert Doctor.inspect_target(nil, cli_target).status == "blocked_missing_cli"
    assert Doctor.inspect_target(nil, auth_target).status == "blocked_missing_auth"
    assert Doctor.inspect_target(nil, launchpad_auth_target).status == "blocked_missing_auth"
    assert Doctor.inspect_target(nil, bugzilla_auth_target).status == "blocked_missing_auth"
  end

  test "doctor uses local config guidance for pkgsrc when the wip user is missing", %{root: root} do
    fake_bin = Path.join(root, "fake-bin")
    write_file(root, "fake-bin/ssh", "#!/bin/sh\nexit 1\n")
    File.chmod!(Path.join(fake_bin, "ssh"), 0o755)
    System.put_env("PATH", "#{fake_bin}:#{System.get_env("PATH")}")

    workspace = Workspace.load!(root)
    result = Doctor.inspect_target(workspace, Workspace.target!(workspace, "pkgsrc"))

    assert result.status == "blocked_missing_config"

    assert Enum.any?(result.messages, fn message ->
             message =~ "config set accounts.pkgsrc_wip_user"
           end)
  end

  test "doctor becomes ready for launchpad and obs-backed targets once local auth state exists",
       %{
         root: root
       } do
    write_file(root, ".local-share/launchpad/bin/python", "#!/bin/sh\nexit 0\n")
    File.chmod!(Path.join(root, ".local-share/launchpad/bin/python"), 0o755)
    write_file(root, ".local-share/launchpad/credentials", "token\n")
    write_file(root, "home/.oscrc", "[general]\nuser = obs-tester\n")

    LocalConfig.set!("accounts.launchpad_owner", "launchpad-tester")
    LocalConfig.set!("accounts.obs_owner", "obs-tester")
    LocalConfig.set!("accounts.obs_project", "home:obs-tester:wireshare")

    workspace = Workspace.load!(root)

    assert Doctor.inspect_target(workspace, Workspace.target!(workspace, "snap")).status == "ok"
    assert Doctor.inspect_target(workspace, Workspace.target!(workspace, "ppa")).status == "ok"
    assert Doctor.inspect_target(workspace, Workspace.target!(workspace, "obs")).status == "ok"

    assert Doctor.inspect_target(workspace, Workspace.target!(workspace, "opensuse")).status ==
             "ok"
  end

  test "submit headless-only includes gated targets and skips manual ones", %{root: root} do
    CLI.main(["prepare", "--cwd", root, "--skip-gradle", "--dry-run"])

    output =
      capture_io(fn ->
        CLI.main(["submit", "--cwd", root, "--headless-only", "--dry-run", "--no-open"])
      end)

    state = Workspace.load!(root) |> State.load()

    assert output =~ "[linux-rollout] submit: starting alpine"
    assert Map.has_key?(state["targets"], "flathub")
    assert Map.has_key?(state["targets"], "homebrew")
    assert Map.has_key?(state["targets"], "haiku")
    assert Map.has_key?(state["targets"], "snap")
    assert Map.has_key?(state["targets"], "pkgsrc")
    refute Map.has_key?(state["targets"], "debian")
  end

  test "github duplicate PR detection accepts existing PR URLs in CLI output" do
    output = """
    pull request create failed: a pull request for branch "tester:linux-rollout-9.9-homebrew" into branch "main" already exists:
    https://github.com/tester/homebrew-wireshare/pull/12
    """

    assert GitSubmit.detect_existing_github_pr(
             output,
             "tester/homebrew-wireshare",
             "tester:linux-rollout-9.9-homebrew",
             "linux-rollout-9.9-homebrew",
             "main"
           ) == %{pull_request: "https://github.com/tester/homebrew-wireshare/pull/12"}
  end

  test "submitter records a target failure and continues with later targets", %{root: root} do
    fake_bin = Path.join(root, "fake-bin")
    write_file(root, "fake-bin/gh", "#!/bin/sh\nexit 0\n")
    File.chmod!(Path.join(fake_bin, "gh"), 0o755)
    System.put_env("PATH", "#{fake_bin}:#{System.get_env("PATH")}")

    workspace = Workspace.load!(root)
    Validator.run!(workspace)

    broken_target = %{
      "name" => "broken",
      "driver" => "unsupported",
      "submission_mode" => "nope",
      "headless_capability" => "full",
      "submission_paths" => ["packaging/does-not-exist"]
    }

    updated_workspace = %{
      workspace
      | targets: Map.put(workspace.targets, "broken", broken_target)
    }

    Submitter.submit!(updated_workspace, ["broken", "homebrew"], dry_run: true, no_open: true)

    state = State.load(updated_workspace)
    assert get_in(state, ["targets", "broken", "status"]) == "failed"
    assert get_in(state, ["targets", "homebrew", "status"]) == "dry_run"
  end

  test "open_url stays headless unless explicitly enabled" do
    assert :ok = Util.open_url("https://example.invalid", [])
    assert :ok = Util.open_url("https://example.invalid", open: false)
    assert :ok = Util.open_url("https://example.invalid", open: true, no_open: true)
  end

  defp write_fixture_workspace(root) do
    write_file(root, "packaging/release_manifest.yaml", """
    app:
      name: WireShare
      id: cx.hermes.WireShare
      vendor: Hermes
      version: 9.9
      legacy_ids:
        primary: org.teamhermes.WireShare
        secondary: org.team_hermes.WireShare
    release:
      tag: release/9.9
      date: 2026-05-20
      notes: |
        Fixture release notes.
    repository:
      slug: example/wireshare
      default_branch: main
      homepage_url: https://example.invalid/wireshare
      issues_url: https://example.invalid/wireshare/issues
    assets:
      source_tarball:
        name: WireShare-9.9-source.tar.gz
        url: https://example.invalid/WireShare-9.9-source.tar.gz
        sha256: ""
        nix_base32: ""
      jar:
        name: WireShare.jar
        url: https://example.invalid/WireShare.jar
        sha256: ""
      checksums:
        name: SHA256SUMS
        url: https://example.invalid/SHA256SUMS
      dependency_inventory:
        name: DEPENDENCY_INVENTORY.txt
        url: https://example.invalid/DEPENDENCY_INVENTORY.txt
    screenshots:
      - path: screenshots/main.png
        caption: Main view
    accounts:
      github_owner: tester
      gitlab_owner: tester
      flathub_fork_repo: tester/flathub
      homebrew_tap_repo: tester/homebrew-wireshare
      homebrew_base_branch: main
      launchpad_owner: tester
      launchpad_project: wireshare
      copr_owner: tester
      submission_name: Test Submitter
      submission_email: tester@example.invalid
      pkgsrc_wip_user: ""
    architectures:
      primary: x86_64
      optional_arm64:
        enabled: false
        jar_url: ""
        jar_sha256: ""
    targets:
      flathub:
        enabled: true
      aur:
        enabled: true
      alpine:
        enabled: true
      obs:
        enabled: true
        destination:
          project: home:tester:wireshare
          package: wireshare
      opensuse:
        enabled: true
        destination:
          project: home:tester:wireshare
          package: wireshare
          target_project: openSUSE:Factory
          target_package: wireshare
      copr:
        enabled: true
        destination:
          owner: tester
          project: wireshare
          package_name: wireshare
          source_repo: tester/wireshare-packaging
          source_base_branch: main
          chroots:
            - fedora-rawhide-x86_64
      homebrew:
        enabled: true
      haiku:
        enabled: true
      snap:
        enabled: true
      ppa:
        enabled: true
      guix:
        enabled: true
      pkgsrc:
        enabled: true
      openbsd:
        enabled: true
      mageia:
        enabled: true
      freebsd:
        enabled: true
      debian:
        enabled: true
    """)

    write_file(root, "packaging/targets.yaml", """
    defaults:
      rendered_root: build/linux-rollout
      branch_template: linux-rollout-@release_version@-@target@
      commit_template: "linux: publish @target@ packaging for @release_version@"
    targets:
      flathub:
        driver: human_gate
        submission_mode: review_packet
        headless_capability: gated
        submission_paths:
          - packaging/common
          - packaging/flathub
        required_files:
          - packaging/flathub/cx.hermes.WireShare.yaml
        destination:
          repo: flathub/flathub
          fork_repo: "@flathub_fork_repo@"
          submission_url: https://example.invalid/flathub
      aur:
        driver: git_submit
        submission_mode: push_only
        headless_capability: full
        submission_paths:
          - packaging/aur
        units:
          - name: wireshare
            required_files:
              - packaging/aur/wireshare/PKGBUILD
              - packaging/aur/wireshare/.SRCINFO
            path_map:
              packaging/aur/wireshare/PKGBUILD: PKGBUILD
              packaging/aur/wireshare/.SRCINFO: .SRCINFO
            destination:
              repo: ssh://aur@aur.archlinux.org/wireshare.git
              base_branch: master
          - name: wireshare-bin
            required_files:
              - packaging/aur/wireshare-bin/PKGBUILD
              - packaging/aur/wireshare-bin/.SRCINFO
            path_map:
              packaging/aur/wireshare-bin/PKGBUILD: PKGBUILD
              packaging/aur/wireshare-bin/.SRCINFO: .SRCINFO
            destination:
              repo: ssh://aur@aur.archlinux.org/wireshare-bin.git
              base_branch: master
      alpine:
        driver: git_submit
        submission_mode: gitlab_mr
        headless_capability: full
        submission_paths:
          - packaging/alpine
        required_files:
          - packaging/alpine/APKBUILD
        path_map:
          packaging/alpine/APKBUILD: community/wireshare/APKBUILD
        destination:
          repo: alpine/aports
          fork_repo: tester/aports
          base_branch: master
      obs:
        driver: service_cli
        service: obs
        submission_mode: obs_home_project
        required_cli:
          - osc
        auth_probe:
          type: file
          path: ~/.oscrc
        headless_capability: full
        submission_paths:
          - packaging/obs
        required_files:
          - packaging/obs/wireshare.spec
        path_map:
          packaging/obs/wireshare.spec: wireshare.spec
        destination:
          project: home:tester:wireshare
          package: wireshare
      opensuse:
        driver: service_cli
        service: obs
        submission_mode: obs_submit_request
        required_cli:
          - osc
        auth_probe:
          type: file
          path: ~/.oscrc
        headless_capability: full
        submission_paths:
          - packaging/opensuse
        required_files:
          - packaging/opensuse/wireshare.spec
        path_map:
          packaging/opensuse/wireshare.spec: wireshare.spec
        destination:
          project: home:tester:wireshare
          package: wireshare
          target_project: openSUSE:Factory
          target_package: wireshare
      copr:
        driver: service_cli
        service: copr
        submission_mode: copr_scm
        headless_capability: full
        submission_paths:
          - packaging/copr
        required_files:
          - packaging/copr/wireshare.spec
        path_map:
          packaging/copr/wireshare.spec: packaging/copr/wireshare.spec
        destination:
          owner: tester
          project: wireshare
          package_name: wireshare
          source_repo: tester/wireshare-packaging
          source_base_branch: main
          chroots:
            - fedora-rawhide-x86_64
      homebrew:
        driver: git_submit
        forge: github
        submission_mode: github_pr
        required_cli:
          - gh
          - git
        auth_probe:
          type: command
          command:
            - gh
            - auth
            - status
        headless_capability: full
        submission_paths:
          - packaging/homebrew
        required_files:
          - packaging/homebrew/wireshare.rb
        path_map:
          packaging/homebrew/wireshare.rb: Formula/wireshare.rb
        destination:
          repo: "@homebrew_tap_repo@"
          fork_repo: "@homebrew_tap_repo@"
          base_branch: "@homebrew_base_branch@"
      haiku:
        driver: git_submit
        forge: github
        submission_mode: github_pr
        required_cli:
          - gh
          - git
        auth_probe:
          type: command
          command:
            - gh
            - auth
            - status
        headless_capability: full
        submission_paths:
          - packaging/haiku
        required_files:
          - packaging/haiku/wireshare.recipe
        path_map:
          packaging/haiku/wireshare.recipe: net-p2p/wireshare/wireshare-@release_version@.recipe
        destination:
          repo: haikuports/haikuports
          fork_repo: tester/haikuports
          base_branch: master
      snap:
        driver: launchpad_api
        submission_mode: launchpad_snap
        required_cli:
          - launchpadlib
        auth_probe:
          type: launchpad_auth
        required_config:
          - destination.owner
          - destination.launchpad_project
          - destination.import_repo_name
          - destination.import_source_url
          - destination.import_branch
          - destination.snap_name
        headless_capability: full
        submission_paths:
          - packaging/common
          - packaging/snap
        required_files:
          - packaging/snap/snapcraft.yaml
        destination:
          owner: "@launchpad_owner@"
          launchpad_project: "@launchpad_project@"
          import_repo_name: wireshare
          import_source_url: https://github.com/@release_repo_slug@.git
          import_branch: "@release_repo_default_branch@"
          snap_name: wireshare
          pocket: Updates
      ppa:
        driver: launchpad_api
        submission_mode: launchpad_ppa
        required_cli:
          - launchpadlib
        auth_probe:
          type: launchpad_auth
        required_config:
          - destination.owner
          - destination.launchpad_project
          - destination.import_repo_name
          - destination.import_source_url
          - destination.import_branch
          - destination.recipe_name
          - destination.ppa_name
          - destination.ubuntu_series
        headless_capability: full
        submission_paths:
          - packaging/common
          - packaging/debian
          - packaging/ppa
        required_files:
          - packaging/ppa/launchpad-recipe.txt
          - packaging/ppa/wireshare.recipe
        destination:
          owner: "@launchpad_owner@"
          launchpad_project: "@launchpad_project@"
          import_repo_name: wireshare
          import_source_url: https://github.com/@release_repo_slug@.git
          import_branch: "@release_repo_default_branch@"
          recipe_name: wireshare
          recipe_description: WireShare Launchpad recipe
          ppa_name: wireshare
          ppa_display_name: WireShare PPA
          ppa_description: WireShare preview builds
          ubuntu_series: noble
          pocket: Updates
      guix:
        driver: mail_submit
        submission_mode: email_patch_thread
        required_cli:
          - git
          - sendmail
        auth_probe:
          - type: git_config
            key: user.name
          - type: git_config
            key: user.email
        headless_capability: full
        submission_paths:
          - packaging/guix
        required_files:
          - packaging/guix/wireshare.scm
        path_map:
          packaging/guix/wireshare.scm: gnu/packages/wireshare.scm
        destination:
          email: guix-patches@gnu.org
      pkgsrc:
        driver: conditional_git_submit
        forge: pkgsrc_wip
        submission_mode: push_only
        required_cli:
          - git
        auth_probe:
          type: command
          command:
            - ssh
            - -o
            - BatchMode=yes
            - -o
            - ConnectTimeout=10
            - -o
            - StrictHostKeyChecking=accept-new
            - -T
            - "@pkgsrc_wip_user@@wip.pkgsrc.org"
          success_codes:
            - 0
            - 1
        required_config:
          - destination.wip_user
        headless_capability: conditional
        submission_paths:
          - packaging/pkgsrc
        required_files:
          - packaging/pkgsrc/ROOT.Makefile
          - packaging/pkgsrc/COMMIT_MSG
          - packaging/pkgsrc/Makefile
          - packaging/pkgsrc/distinfo
          - packaging/pkgsrc/DESCR
          - packaging/pkgsrc/PLIST
        path_map:
          packaging/pkgsrc/ROOT.Makefile: Makefile
          packaging/pkgsrc/COMMIT_MSG: COMMIT_MSG
          packaging/pkgsrc/Makefile: wireshare/Makefile
          packaging/pkgsrc/distinfo: wireshare/distinfo
          packaging/pkgsrc/DESCR: wireshare/DESCR
          packaging/pkgsrc/PLIST: wireshare/PLIST
        destination:
          wip_user: "@pkgsrc_wip_user@"
          repo: "@pkgsrc_wip_user@@wip.pkgsrc.org:/pkgsrc-wip.git"
          base_branch: master
          submission_url: https://example.invalid/pkgsrc
      openbsd:
        driver: mail_submit
        submission_mode: email_attachment
        required_cli:
          - sendmail
        auth_probe:
          - type: git_config
            key: user.name
          - type: git_config
            key: user.email
        headless_capability: full
        submission_paths:
          - packaging/common
          - packaging/openbsd
        required_files:
          - packaging/openbsd/Makefile
          - packaging/openbsd/distinfo
          - packaging/openbsd/pkg/DESCR
          - packaging/openbsd/pkg/PLIST
        destination:
          email: ports@openbsd.org
      mageia:
        driver: bugzilla_api
        submission_mode: bugzilla_request
        headless_capability: full
        submission_paths:
          - packaging/common
          - packaging/mageia
        required_files:
          - packaging/mageia/wireshare.spec
        path_map:
          packaging/mageia/wireshare.spec: wireshare.spec
        destination:
          bugzilla_url: https://bugs.mageia.org
          product: Mageia
          component: New RPM package request
          version: Cauldron
          platform: All
          severity: Enhancement
          summary: wireshare request
      freebsd:
        driver: bugzilla_api
        submission_mode: bugzilla_patch
        headless_capability: full
        submission_paths:
          - packaging/common
          - packaging/freebsd
        required_files:
          - packaging/freebsd/Makefile
          - packaging/freebsd/distinfo
          - packaging/freebsd/pkg-descr
          - packaging/freebsd/pkg-plist
        path_map:
          packaging/freebsd/Makefile: net-p2p/wireshare/Makefile
          packaging/freebsd/distinfo: net-p2p/wireshare/distinfo
          packaging/freebsd/pkg-descr: net-p2p/wireshare/pkg-descr
          packaging/freebsd/pkg-plist: net-p2p/wireshare/pkg-plist
        destination:
          bugzilla_url: https://bugs.freebsd.org/bugzilla
          product: Ports & Packages
          component: Individual Port(s)
          version: Latest
          platform: Any
          severity: Affects Only Me
          summary: "[NEW PORT] net-p2p/wireshare"
      debian:
        driver: manual_handoff
        submission_mode: manual_packet
        headless_capability: manual
        submission_paths:
          - packaging/debian
        required_files:
          - packaging/debian/control
        destination:
          submission_url: https://example.invalid/debian
    """)

    write_file(root, "packaging/common/app/cx.hermes.WireShare.desktop", """
    [Desktop Entry]
    Name=@release_app_name@
    Icon=@release_app_id@
    """)

    write_file(root, "packaging/common/app/cx.hermes.WireShare.metainfo.xml", """
    <component>
      <id>@release_app_id@</id>
      <url type="homepage">@release_homepage_url@</url>
    </component>
    """)

    write_file(
      root,
      "packaging/common/launchers/WireShare",
      "#!/bin/sh\nexec java -jar /usr/share/wireshare/WireShare.jar \"$@\"\n"
    )

    write_file(
      root,
      "packaging/flathub/cx.hermes.WireShare.yaml",
      "id: @release_app_id@\nsha256: @source_tarball_sha256@\n"
    )

    write_file(root, "packaging/aur/wireshare/PKGBUILD", "pkgver=@release_version@\n")
    write_file(root, "packaging/aur/wireshare/.SRCINFO", "pkgbase = wireshare\n")
    write_file(root, "packaging/aur/wireshare-bin/PKGBUILD", "pkgver=@release_version@\n")
    write_file(root, "packaging/aur/wireshare-bin/.SRCINFO", "pkgbase = wireshare-bin\n")

    write_file(root, "packaging/alpine/APKBUILD", """
    pkgver=@release_version@
    sha256sums="@source_tarball_sha256@  $pkgname-$pkgver.tar.gz"
    """)

    write_file(root, "packaging/obs/wireshare.spec", "Version: @release_version@\n")
    write_file(root, "packaging/opensuse/wireshare.spec", "Version: @release_version@\n")
    write_file(root, "packaging/copr/wireshare.spec", "Version: @release_version@\n")

    write_file(
      root,
      "packaging/homebrew/wireshare.rb",
      "class Wireshare < Formula\n  sha256 \"@source_tarball_sha256@\"\nend\n"
    )

    write_file(root, "packaging/haiku/wireshare.recipe", "SUMMARY=\"WireShare\"\n")

    write_file(root, "packaging/snap/snapcraft.yaml", "name: wireshare\n")
    write_file(root, "packaging/ppa/launchpad-recipe.txt", "owner=@launchpad_owner@\n")

    write_file(
      root,
      "packaging/ppa/wireshare.recipe",
      "# git-build-recipe format 0.4 deb-version @release_version@+{revtime}-0~ppa1\nlp:~@launchpad_owner@/@launchpad_project@/+git/wireshare\n"
    )

    write_file(
      root,
      "packaging/guix/wireshare.scm",
      "(define-public wireshare '@source_tarball_nix_base32@)\n"
    )

    write_file(root, "packaging/pkgsrc/ROOT.Makefile", "SUBDIR+= wireshare\n")

    write_file(
      root,
      "packaging/pkgsrc/COMMIT_MSG",
      "net/wireshare: import wireshare-@release_version@\n"
    )

    write_file(
      root,
      "packaging/pkgsrc/Makefile",
      "DISTNAME= WireShare-@release_version@-source\n"
    )

    write_file(
      root,
      "packaging/pkgsrc/distinfo",
      "SHA512 (WireShare-@release_version@-source.tar.gz) = @source_tarball_sha512@\nSize (WireShare-@release_version@-source.tar.gz) = @source_tarball_size@ bytes\n"
    )

    write_file(root, "packaging/pkgsrc/DESCR", "WireShare\n")
    write_file(root, "packaging/pkgsrc/PLIST", "bin/WireShare\n")

    write_file(
      root,
      "packaging/openbsd/Makefile",
      "DISTNAME = WireShare-@release_version@-source\n"
    )

    write_file(
      root,
      "packaging/openbsd/distinfo",
      "SHA256 (WireShare-@release_version@-source.tar.gz) = @source_tarball_sha256@\nSIZE (WireShare-@release_version@-source.tar.gz) = @source_tarball_size@\n"
    )

    write_file(root, "packaging/openbsd/pkg/DESCR", "WireShare\n")
    write_file(root, "packaging/openbsd/pkg/PLIST", "bin/WireShare\n")
    write_file(root, "packaging/mageia/wireshare.spec", "Version: @release_version@\n")

    write_file(
      root,
      "packaging/freebsd/Makefile",
      "PORTNAME=\twireshare\nDISTVERSION=\t@release_version@\n"
    )

    write_file(
      root,
      "packaging/freebsd/distinfo",
      "TIMESTAMP = 0\nSHA256 (WireShare-@release_version@-source.tar.gz) = @source_tarball_sha256@\nSIZE (WireShare-@release_version@-source.tar.gz) = @source_tarball_size@\n"
    )

    write_file(root, "packaging/freebsd/pkg-descr", "WireShare\n")
    write_file(root, "packaging/freebsd/pkg-plist", "bin/WireShare\n")
    write_file(root, "packaging/debian/control", "Source: wireshare\n")

    write_file(root, "tools/linux_rollout/scripts/launchpad_helper.py", """
    #!/usr/bin/env python3
    import json
    import sys

    command = sys.argv[1]

    if command == 'auth-login':
        print('Authenticated to Launchpad as Launchpad Tester (~launchpad-tester)')
    elif command == 'auth-logout':
        print('Logged out')
    elif command == 'whoami':
        print(json.dumps({'name': 'launchpad-tester', 'display_name': 'Launchpad Tester'}))
    else:
        print('{}')
    """)

    write_file(root, "WireShare.jar", "jar")
    write_file(root, "build/release-artifacts/WireShare-9.9-source.tar.gz", "src")

    write_file(root, "build/release-artifacts/SHA256SUMS", """
    #{String.duplicate("a", 64)}  WireShare-9.9-source.tar.gz
    #{String.duplicate("b", 64)}  WireShare.jar
    """)

    write_file(root, "build/release-artifacts/DEPENDENCY_INVENTORY.txt", "deps\n")
  end

  defp write_file(root, relative_path, contents) do
    path = Path.join(root, relative_path)
    File.mkdir_p!(Path.dirname(path))
    File.write!(path, contents)
  end
end
