# typed: false
# frozen_string_literal: true

require "test/support/fixtures/testball"
require "formula"

PHASES = [:build, :postinstall, :test].freeze

# These tests need to duplicate methods.
RSpec.describe Formula do
  alias_matcher :follow_installed_alias, :be_follow_installed_alias
  alias_matcher :have_any_version_installed, :be_any_version_installed
  alias_matcher :need_migration, :be_migration_needed

  alias_matcher :have_changed_installed_alias_target, :be_installed_alias_target_changed
  alias_matcher :supersede_an_installed_formula, :be_supersedes_an_installed_formula
  alias_matcher :have_changed_alias, :be_alias_changed

  alias_matcher :have_option_defined, :be_option_defined
  alias_matcher :have_post_install_defined, :be_post_install_defined
  alias_matcher :have_test_defined, :be_test_defined
  alias_matcher :pour_bottle, :be_pour_bottle

  describe "::new" do
    let(:klass) do
      Class.new(described_class) do
        url "https://brew.sh/foo-1.0.tar.gz"
      end
    end

    let(:name) { "formula_name" }
    let(:path) { Formulary.core_path(name) }
    let(:spec) { :stable }
    let(:alias_name) { "baz@1" }
    let(:alias_path) { CoreTap.instance.alias_dir/alias_name }
    let(:f) { klass.new(name, path, spec) }
    let(:f_alias) { klass.new(name, path, spec, alias_path:) }

    specify "formula instantiation" do
      expect(f.name).to eq(name)
      expect(f.specified_name).to eq(name)
      expect(f.full_name).to eq(name)
      expect(f.full_specified_name).to eq(name)
      expect(f.path).to eq(path)
      expect(f.alias_path).to be_nil
      expect(f.alias_name).to be_nil
      expect(f.full_alias_name).to be_nil
      expect(f.specified_path).to eq(path)
      [:build, :test, :postinstall].each { |phase| expect(f.network_access_allowed?(phase)).to be(true) }
      expect { klass.new }.to raise_error(ArgumentError)
    end

    specify "formula instantiation with alias" do
      expect(f_alias.name).to eq(name)
      expect(f_alias.full_name).to eq(name)
      expect(f_alias.path).to eq(path)
      expect(f_alias.alias_path).to eq(alias_path)
      expect(f_alias.alias_name).to eq(alias_name)
      expect(f_alias.specified_name).to eq(alias_name)
      expect(f_alias.specified_path).to eq(Pathname(alias_path))
      expect(f_alias.full_alias_name).to eq(alias_name)
      expect(f_alias.full_specified_name).to eq(alias_name)
      [:build, :test, :postinstall].each { |phase| expect(f_alias.network_access_allowed?(phase)).to be(true) }
      expect { klass.new }.to raise_error(ArgumentError)
    end

    specify "formula instantiation without a subclass" do
      expect { described_class.new(name, path, spec) }
        .to raise_error(RuntimeError, "Do not call `Formula.new' directly without a subclass.")
    end

    context "when in a Tap" do
      let(:tap) { Tap.fetch("foo", "bar") }
      let(:path) { tap.path/"Formula/#{name}.rb" }
      let(:full_name) { "#{tap.user}/#{tap.repository}/#{name}" }
      let(:full_alias_name) { "#{tap.user}/#{tap.repository}/#{alias_name}" }

      specify "formula instantiation" do
        expect(f.name).to eq(name)
        expect(f.specified_name).to eq(name)
        expect(f.full_name).to eq(full_name)
        expect(f.full_specified_name).to eq(full_name)
        expect(f.path).to eq(path)
        expect(f.alias_path).to be_nil
        expect(f.alias_name).to be_nil
        expect(f.full_alias_name).to be_nil
        expect(f.specified_path).to eq(path)
        expect { klass.new }.to raise_error(ArgumentError)
      end

      specify "formula instantiation with alias" do
        expect(f_alias.name).to eq(name)
        expect(f_alias.full_name).to eq(full_name)
        expect(f_alias.path).to eq(path)
        expect(f_alias.alias_path).to eq(alias_path)
        expect(f_alias.alias_name).to eq(alias_name)
        expect(f_alias.specified_name).to eq(alias_name)
        expect(f_alias.specified_path).to eq(Pathname(alias_path))
        expect(f_alias.full_alias_name).to eq(full_alias_name)
        expect(f_alias.full_specified_name).to eq(full_alias_name)
        expect { klass.new }.to raise_error(ArgumentError)
      end
    end
  end

  describe "#follow_installed_alias?" do
    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    it "returns true by default" do
      expect(f).to follow_installed_alias
    end

    it "can be set to true" do
      f.follow_installed_alias = true
      expect(f).to follow_installed_alias
    end

    it "can be set to false" do
      f.follow_installed_alias = false
      expect(f).not_to follow_installed_alias
    end
  end

  describe "#versioned_formula?" do
    let(:f) do
      formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    let(:f2) do
      formula "foo@2.0" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end
    end

    specify do
      expect(f2.versioned_formula?).to be true
      expect(f.versioned_formula?).to be false
    end
  end

  describe ".python_major_minor_version" do
    it "delegates to Language::Python.major_minor_version" do
      version = instance_double(Version, "version")
      expect(Language::Python).to receive(:major_minor_version).with("python3").and_return(version)

      expect(described_class.python_major_minor_version("python3")).to be(version)
    end
  end

  describe "#versioned_formulae" do
    let(:f) do
      formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    let(:f2) do
      formula "foo@2.0" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end
    end

    let(:f_full) do
      formula "foo-full" do
        T.bind(self, T.class_of(Formula))
        url "foo-full-1.0"
      end
    end

    let(:f_full2) do
      formula "foo@2.0-full" do
        T.bind(self, T.class_of(Formula))
        url "foo-full-2.0"
      end
    end

    before do
      # don't try to load/fetch gcc/glibc
      allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false)

      allow(Formulary).to receive(:load_formula_from_path)
        .with(f2.name, f2.path, flags: [], ignore_errors: false).and_return(f2)
      allow(Formulary).to receive(:factory).with(f2.name).and_return(f2)
      allow(Formulary).to receive(:load_formula_from_path)
        .with(f_full2.name, f_full2.path, flags: [], ignore_errors: false).and_return(f_full2)
      allow(Formulary).to receive(:factory).with(f_full2.name).and_return(f_full2)
      allow(f).to receive(:versioned_formulae_names).and_return([f2.name])
    end

    it "returns array with versioned formulae" do
      FileUtils.touch f.path
      FileUtils.touch f2.path
      expect(f.versioned_formulae).to eq [f2]
    end

    it "returns empty array for non-@-versioned formulae" do
      FileUtils.touch f.path
      FileUtils.touch f2.path
      expect(f2.versioned_formulae).to be_empty
    end

    it "returns versioned full formulae for the matching full formula" do
      allow(f_full).to receive(:tap).and_return(nil)
      FileUtils.touch f_full.path
      FileUtils.touch f_full2.path
      expect(f_full.versioned_formulae).to eq [f_full2]
    end
  end

  describe "#full_formulae_names" do
    let(:f) do
      formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    let(:f_full) do
      formula "foo-full" do
        T.bind(self, T.class_of(Formula))
        url "foo-full-1.0"
      end
    end

    let(:f_versioned) do
      formula "foo@2.0" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end
    end

    let(:f_versioned_full) do
      formula "foo@2.0-full" do
        T.bind(self, T.class_of(Formula))
        url "foo-full-2.0"
      end
    end

    before do
      [f, f_full, f_versioned].each do |formula|
        allow(formula).to receive(:tap).and_return(nil)
        FileUtils.touch formula.path
      end
    end

    it "returns only existing sibling full and non-full names" do
      expect(f.full_formulae_names).to eq ["foo-full"]
      expect(f_full.full_formulae_names).to eq ["foo"]
      expect(f_versioned.full_formulae_names).to eq []

      allow(f_versioned_full).to receive(:tap).and_return(nil)
      FileUtils.touch f_versioned_full.path
      f_versioned_with_full = formula "foo@2.0" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end
      allow(f_versioned_with_full).to receive(:tap).and_return(nil)
      FileUtils.touch f_versioned_with_full.path

      expect(f_versioned_with_full.full_formulae_names).to eq ["foo@2.0-full"]
    end
  end

  describe "#unversioned_formula_name" do
    let(:f) do
      formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    let(:f_full) do
      formula "foo@2.0-full" do
        T.bind(self, T.class_of(Formula))
        url "foo-full-2.0"
      end
    end

    let(:f_versioned) do
      formula "foo@2.0" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end
    end

    it "returns the matching unversioned sibling name" do
      expect(f.unversioned_formula_name).to be_nil
      expect(f_versioned.unversioned_formula_name).to eq("foo")
      expect(f_full.unversioned_formula_name).to eq("foo-full")
    end
  end

  describe "#link_overwrite_reason" do
    it "explains why a formula was not linked" do
      f = formula "foo@2.0" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end
      other_formula = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
      allow(other_formula).to receive_messages(any_version_installed?: true, linked?: true)
      allow(f).to receive(:link_overwrite_formulae).and_return([other_formula])

      expect(f.link_overwrite_reason).to eq("foo is already linked")
    end
  end

  describe "#link_overwrite_formulae" do
    it "deduplicates formulae shared by an alias and canonical name" do
      f = formula "foo@2.0" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end
      other_formula = formula "foo@1.0" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
      allow(f).to receive(:link_overwrite_formulae_names).and_return(["foo", "foo@1.0"])
      allow(Formulary).to receive(:factory).with("foo").and_return(other_formula)
      allow(Formulary).to receive(:factory).with("foo@1.0").and_return(other_formula)

      expect(f.link_overwrite_formulae).to eq([other_formula])
    end
  end

  describe "#link_overwrite_formulae_names" do
    let(:f) do
      formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    let(:f_full) do
      formula "foo-full" do
        T.bind(self, T.class_of(Formula))
        url "foo-full-1.0"
      end
    end

    let(:f_versioned) do
      formula "foo@2.0" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end
    end

    let(:f_versioned_full) do
      formula "foo@2.0-full" do
        T.bind(self, T.class_of(Formula))
        url "foo-full-2.0"
      end
    end

    before do
      [f, f_full, f_versioned, f_versioned_full].each do |formula|
        allow(Formulary).to receive(:load_formula_from_path)
          .with(formula.name, formula.path, flags: [], ignore_errors: false).and_return(formula)
        allow(Formulary).to receive(:factory).with(formula.name).and_return(formula)
        FileUtils.touch formula.path
      end

      allow(f).to receive_messages(versioned_formulae_names: [f_versioned.name], full_formulae_names: [f_full.name])
      allow(f_full).to receive_messages(versioned_formulae_names: [], full_formulae_names: [f.name])
      allow(f_versioned).to receive_messages(versioned_formulae_names: [],
                                             full_formulae_names:      [f_versioned_full.name])
      allow(f_versioned_full).to receive_messages(versioned_formulae_names: [],
                                                  full_formulae_names:      [f_versioned.name])
    end

    it "includes direct full and unversioned siblings while excluding the current formula" do
      expect(f_versioned.link_overwrite_formulae_names)
        .to eq(["foo", f_full.name, f_versioned_full.name])
    end
  end

  describe "#link_overwrite?" do
    let(:versioned_formula) do
      formula "foo@22" do
        T.bind(self, T.class_of(Formula))
        url "foo-22.0"
      end
    end

    let(:related_formula) do
      formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    let(:conflict_file) { HOMEBREW_PREFIX/"lib/formula_spec/node_modules/npm/LICENSE" }

    before do
      allow(versioned_formula).to receive(:link_overwrite_formulae).and_return([related_formula])
      conflict_file.dirname.mkpath
      FileUtils.touch conflict_file
    end

    after do
      FileUtils.rm_f conflict_file
      conflict_file.dirname.rmdir_if_possible
      conflict_file.dirname.parent.rmdir_if_possible
      conflict_file.dirname.parent.parent.rmdir_if_possible
    end

    it "does not allow untracked conflicts for related formula families" do
      expect(versioned_formula.link_overwrite?(conflict_file)).to be false
    end

    it "returns false when the conflict is not Homebrew-managed" do
      allow(versioned_formula).to receive(:link_overwrite_keg_name).and_return(nil)

      expect(versioned_formula.link_overwrite?(HOMEBREW_PREFIX/"bin/foo")).to be false
    end

    it "returns false for ambiguous keg names" do
      allow(versioned_formula).to receive(:link_overwrite_keg_name).and_return("foo")
      ambiguity_loaders = [
        instance_double(Formulary::FormulaLoader, tap: instance_double(Tap, to_s: "homebrew/core")),
        instance_double(Formulary::FormulaLoader, tap: instance_double(Tap, to_s: "homebrew/other")),
      ]
      allow(Formulary).to receive(:factory).with("foo")
                                           .and_raise(TapFormulaAmbiguityError.new("foo", ambiguity_loaders))

      expect(versioned_formula.link_overwrite?(HOMEBREW_PREFIX/"bin/foo")).to be false
    end

    it "returns false for unrelated keg names" do
      unrelated_formula = formula "bar" do
        T.bind(self, T.class_of(Formula))
        url "bar-1.0"
      end
      allow(versioned_formula).to receive(:link_overwrite_keg_name).and_return("bar")
      allow(Formulary).to receive(:factory).with("bar").and_return(unrelated_formula)
      allow(unrelated_formula).to receive(:possible_names).and_return(["baz"])

      expect(versioned_formula.link_overwrite?(HOMEBREW_PREFIX/"bin/bar")).to be false
    end

    it "allows explicit link_overwrite paths" do
      formula_with_explicit_overwrite = formula "baz" do
        T.bind(self, T.class_of(Formula))
        url "baz-1.0"
        link_overwrite "bin/baz"
      end
      allow(formula_with_explicit_overwrite).to receive(:link_overwrite_keg_name).and_return("baz")
      allow(Formulary).to receive(:factory).with("baz").and_return(formula_with_explicit_overwrite)

      expect(formula_with_explicit_overwrite.link_overwrite?(HOMEBREW_PREFIX/"bin/baz")).to be true
    end

    it "allows existing related keg names through implied overwrites" do
      allow(versioned_formula).to receive(:link_overwrite_keg_name).and_return("foo")
      allow(Formulary).to receive(:factory).with("foo").and_return(related_formula)

      expect(versioned_formula.link_overwrite?(HOMEBREW_PREFIX/"bin/foo")).to be true
    end

    it "allows deleted related keg names through implied overwrites" do
      allow(versioned_formula).to receive(:link_overwrite_keg_name).and_return("foo-old")
      allow(Formulary).to receive(:factory).with("foo-old").and_raise(FormulaUnavailableError.new("foo-old"))
      allow(related_formula).to receive_messages(oldnames: ["foo-old"], aliases: [])

      expect(versioned_formula.link_overwrite?(HOMEBREW_PREFIX/"bin/foo")).to be true
    end

    it "returns false for missing conflicts without explicit or implied overwrites" do
      formula_without_overwrites = formula "qux" do
        T.bind(self, T.class_of(Formula))
        url "qux-1.0"
      end
      allow(formula_without_overwrites).to receive_messages(link_overwrite_keg_name: :missing,
                                                            link_overwrite_formulae: [])

      expect(formula_without_overwrites.link_overwrite?(HOMEBREW_PREFIX/"bin/qux")).to be false
    end
  end

  describe "#implied_link_overwrite?" do
    let(:versioned_formula) do
      formula "foo@22" do
        T.bind(self, T.class_of(Formula))
        url "foo-22.0"
      end
    end

    let(:related_formula) do
      formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    before do
      allow(related_formula).to receive_messages(oldnames: ["foo-old"], aliases: ["foo-alias"])
    end

    it "does not allow missing conflicts without actual related formulae" do
      expect(versioned_formula.implied_link_overwrite?(:missing, [])).to be false
    end

    it "does not allow non-Homebrew conflicts" do
      expect(versioned_formula.implied_link_overwrite?(nil, [related_formula])).to be false
    end

    it "does not allow missing conflicts even when related formulae exist" do
      expect(versioned_formula.implied_link_overwrite?(:missing, [related_formula])).to be false
    end

    it "allows related keg names via oldnames" do
      expect(versioned_formula.implied_link_overwrite?("foo-old", [related_formula])).to be true
    end

    it "allows related keg names via aliases" do
      expect(versioned_formula.implied_link_overwrite?("foo-alias", [related_formula])).to be true
    end

    it "does not allow unrelated keg names" do
      expect(versioned_formula.implied_link_overwrite?("bar", [related_formula])).to be false
    end
  end

  example "installed alias with core" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"
    end

    build_values_with_no_installed_alias = [
      BuildOptions.new(Options.new, f.options),
      Tab.new(source: { "path" => f.path.to_s }),
    ]
    build_values_with_no_installed_alias.each do |build|
      f.build = build
      expect(f.installed_alias_path).to be_nil
      expect(f.installed_alias_name).to be_nil
      expect(f.full_installed_alias_name).to be_nil
      expect(f.full_installed_specified_name).to eq(f.name)
    end

    alias_name = "bar"
    alias_path = CoreTap.instance.alias_dir/alias_name
    CoreTap.instance.alias_dir.mkpath
    FileUtils.ln_sf f.path, alias_path

    f.build = Tab.new(source: { "path" => alias_path.to_s })

    expect(f.installed_alias_path).to eq(alias_path)
    expect(f.installed_alias_name).to eq(alias_name)
    expect(f.full_installed_alias_name).to eq(alias_name)
    expect(f.full_installed_specified_name).to eq(alias_name)
  end

  example "installed alias with tap" do
    tap = Tap.fetch("user", "repo")
    name = "foo"
    path = tap.path/"Formula/#{name}.rb"
    f = formula(name, path:) do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"
    end

    build_values_with_no_installed_alias = [
      BuildOptions.new(Options.new, f.options),
      Tab.new(source: { "path" => f.path.to_s }),
    ]
    build_values_with_no_installed_alias.each do |build|
      f.build = build
      expect(f.installed_alias_path).to be_nil
      expect(f.installed_alias_name).to be_nil
      expect(f.full_installed_alias_name).to be_nil
      expect(f.full_installed_specified_name).to eq(f.full_name)
    end

    alias_name = "bar"
    alias_path = tap.alias_dir/alias_name
    full_alias_name = "#{tap.user}/#{tap.repository}/#{alias_name}"
    tap.alias_dir.mkpath
    FileUtils.ln_sf f.path, alias_path

    f.build = Tab.new(source: { "path" => alias_path.to_s })

    expect(f.installed_alias_path).to eq(alias_path)
    expect(f.installed_alias_name).to eq(alias_name)
    expect(f.full_installed_alias_name).to eq(full_alias_name)
    expect(f.full_installed_specified_name).to eq(full_alias_name)

    FileUtils.rm_rf HOMEBREW_LIBRARY/"Taps/user"
  end

  specify "#prefix" do
    f = Testball.new
    expect(f.prefix).to eq(HOMEBREW_CELLAR/f.name/"0.1")
    expect(f.prefix).to be_a(Pathname)
  end

  example "revised prefix" do
    f = Class.new(Testball) { revision(1) }.new
    expect(f.prefix).to eq(HOMEBREW_CELLAR/f.name/"0.1_1")
  end

  example "compatibility_version" do
    f = Class.new(Testball) { compatibility_version(1) }.new
    expect(f.class.compatibility_version).to eq(1)
  end

  specify "#any_version_installed?" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo"
      version "1.0"
    end

    expect(f).not_to have_any_version_installed

    prefix = HOMEBREW_CELLAR/f.name/"0.1"
    prefix.mkpath
    FileUtils.touch prefix/AbstractTab::FILENAME

    expect(f).to have_any_version_installed
  end

  specify "#formula_opt_bin" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo"
      version "1.0"
    end

    expect(f.formula_opt_bin("foo")).to eq(HOMEBREW_PREFIX/"opt/foo/bin")
  end

  specify "#migration_needed" do
    f = Testball.new("newname")
    f.oldnames = ["oldname"]
    f.tap = CoreTap.instance

    oldname_prefix = (HOMEBREW_CELLAR/"oldname/2.20")
    newname_prefix = (HOMEBREW_CELLAR/"newname/2.10")

    oldname_prefix.mkpath
    oldname_tab = Tab.empty
    oldname_tab.tabfile = oldname_prefix/AbstractTab::FILENAME
    oldname_tab.write

    expect(f).not_to need_migration

    oldname_tab.tabfile.unlink
    oldname_tab.source["tap"] = "homebrew/core"
    oldname_tab.write

    expect(f).to need_migration

    newname_prefix.mkpath

    expect(f).not_to need_migration
  end

  specify "#oldnames ignores same-name cask-to-formula migrations" do
    tap = Tap.fetch("homebrew", "foo")
    allow(Tap).to receive(:tap_migration_oldnames).with(tap, "same-name-cask")
                                                  .and_return(["same-name-cask"])

    expect(formula("same-name-cask", tap:) do
      T.bind(self, T.class_of(Formula))
      url "https://brew.sh/same-name-cask-1.0.tar.gz"
    end.oldnames).to be_empty
  end

  describe "#latest_version_installed?" do
    let(:f) { Testball.new }

    it "returns false if the #latest_installed_prefix is not a directory" do
      allow(f).to receive(:latest_installed_prefix).and_return(instance_double(Pathname, directory?: false))
      expect(f).not_to be_latest_version_installed
    end

    it "returns false if the #latest_installed_prefix is empty" do
      allow(f).to receive(:latest_installed_prefix)
        .and_return(instance_double(Pathname, directory?: true, empty?: true))
      expect(f).not_to be_latest_version_installed
    end

    it "returns true if the #latest_installed_prefix is not empty" do
      allow(f).to receive(:latest_installed_prefix)
        .and_return(instance_double(Pathname, directory?: true, empty?: false))
      expect(f).to be_latest_version_installed
    end
  end

  describe "#latest_installed_prefix" do
    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo"
        version "1.9"
        head "foo"
      end
    end

    let(:stable_prefix) { HOMEBREW_CELLAR/f.name/f.version }
    let(:head_prefix) { HOMEBREW_CELLAR/f.name/f.head.version }

    it "is the same as #prefix by default" do
      expect(f.latest_installed_prefix).to eq(f.prefix)
    end

    it "returns the stable prefix if it is installed" do
      stable_prefix.mkpath
      expect(f.latest_installed_prefix).to eq(stable_prefix)
    end

    it "returns the head prefix if it is installed" do
      head_prefix.mkpath
      expect(f.latest_installed_prefix).to eq(head_prefix)
    end

    it "returns the stable prefix if head is outdated" do
      head_prefix.mkpath

      tab = Tab.empty
      tab.tabfile = head_prefix/AbstractTab::FILENAME
      tab.source["versions"] = { "stable" => "1.0" }
      tab.write

      expect(f.latest_installed_prefix).to eq(stable_prefix)
    end

    it "returns the head prefix if the active specification is :head" do
      f.active_spec = :head
      expect(f.latest_installed_prefix).to eq(head_prefix)
    end
  end

  describe "#latest_head_prefix" do
    let(:f) { Testball.new }

    it "returns the latest head prefix" do
      stamps_with_revisions = [
        [111111, 1],
        [222222, 0],
        [222222, 1],
        [222222, 2],
      ]

      stamps_with_revisions.each do |stamp, revision|
        version = "HEAD-#{stamp}"
        version = "#{version}_#{revision}" unless revision.zero?

        prefix = f.rack/version
        prefix.mkpath

        tab = Tab.empty
        tab.tabfile = prefix/AbstractTab::FILENAME
        tab.source_modified_time = stamp
        tab.write
      end

      prefix = HOMEBREW_CELLAR/f.name/"HEAD-222222_2"

      expect(f.latest_head_prefix).to eq(prefix)
    end
  end

  specify "equality" do
    x = Testball.new
    y = Testball.new

    expect(x).to eq(y)
    expect(x).to eql(y)
    expect(x.hash).to eq(y.hash)
  end

  specify "inequality" do
    x = Testball.new("foo")
    y = Testball.new("bar")

    expect(x).not_to eq(y)
    expect(x).not_to eql(y)
    expect(x.hash).not_to eq(y.hash)
  end

  specify "comparison with non formula objects does not raise" do
    expect(Object.new).not_to eq(Testball.new)
  end

  specify "#<=>" do
    expect(Testball.new <=> Object.new).to be_nil
  end

  describe "#installed_alias_path" do
    example "alias paths with build options" do
      alias_path = (CoreTap.instance.alias_dir/"another_name")

      f = formula(alias_path:) do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
      f.build = BuildOptions.new(Options.new, f.options)

      expect(f.alias_path).to eq(alias_path)
      expect(f.installed_alias_path).to be_nil
    end

    example "alias paths with tab with non alias source path" do
      alias_path = (CoreTap.instance.alias_dir/"another_name")
      source_path = CoreTap.instance.new_formula_path("another_other_name")

      f = formula(alias_path:) do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
      f.build = Tab.new(source: { "path" => source_path.to_s })

      expect(f.alias_path).to eq(alias_path)
      expect(f.installed_alias_path).to be_nil
    end

    example "alias paths with tab with alias source path" do
      alias_path = (CoreTap.instance.alias_dir/"another_name")
      source_path = (CoreTap.instance.alias_dir/"another_other_name")

      f = formula(alias_path:) do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
      f.build = Tab.new(source: { "path" => source_path.to_s })
      CoreTap.instance.alias_dir.mkpath
      FileUtils.ln_sf f.path, source_path

      expect(f.alias_path).to eq(alias_path)
      expect(f.installed_alias_path).to eq(source_path)
    end
  end

  describe "::inreplace" do
    specify "raises build error on failure" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "https://brew.sh/test-1.0.tbz"
      end

      expect { f.inreplace([]) }.to raise_error(BuildError)
    end

    specify "replaces text in file" do
      file = Tempfile.new("test")
      File.binwrite(file, <<~EOS)
        ab
        bc
        cd
      EOS
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "https://brew.sh/test-1.0.tbz"
      end
      f.inreplace(file.path) do |s|
        s.gsub!("bc", "yz")
      end
      expect(File.binread(file)).to eq <<~EOS
        ab
        yz
        cd
      EOS
    end
  end

  describe "::installed_with_alias_path" do
    specify "with alias path with nil" do
      expect(described_class.installed_with_alias_path(nil)).to be_empty
    end

    specify "with alias path with a path" do
      alias_path = CoreTap.instance.alias_dir/"alias"
      different_alias_path = CoreTap.instance.alias_dir/"another_alias"

      formula_with_alias = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
      formula_with_alias.build = Tab.empty
      formula_with_alias.build.source["path"] = alias_path.to_s

      formula_without_alias = formula "bar" do
        T.bind(self, T.class_of(Formula))
        url "bar-1.0"
      end
      formula_without_alias.build = Tab.empty
      formula_without_alias.build.source["path"] = formula_without_alias.path.to_s

      formula_with_different_alias = formula "baz" do
        T.bind(self, T.class_of(Formula))
        url "baz-1.0"
      end
      formula_with_different_alias.build = Tab.empty
      formula_with_different_alias.build.source["path"] = different_alias_path.to_s

      formulae = [
        formula_with_alias,
        formula_without_alias,
        formula_with_different_alias,
      ]

      allow(described_class).to receive(:installed).and_return(formulae)

      CoreTap.instance.alias_dir.mkpath
      FileUtils.ln_sf formula_with_alias.path, alias_path

      expect(described_class.installed_with_alias_path(alias_path))
        .to eq([formula_with_alias])
    end
  end

  specify ".url" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"
    end

    expect(f.class.url).to eq("foo-1.0")
  end

  specify ".homepage with a human browser check" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      homepage "https://brew.sh", browsed: "2026-07-26"
      url "https://brew.sh/test-1.0.tar.gz"
    end

    expect(f.homepage_browsed).to eq(Date.new(2026, 7, 26))
  end

  specify ".homepage requires a URL with a human browser check" do
    expect do
      formula do
        T.bind(self, T.class_of(Formula))
        homepage browsed: "2026-07-26"
      end
    end.to raise_error(ArgumentError, "`browsed` requires a homepage URL")
  end

  specify "spec integration" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      homepage "https://brew.sh"

      url "https://brew.sh/test-0.1.tbz"
      mirror "https://example.org/test-0.1.tbz"
      sha256 TEST_SHA256

      head "https://brew.sh/test.git", tag: "foo"
    end

    expect(f.homepage).to eq("https://brew.sh")
    expect(f.version).to eq(Version.new("0.1"))
    expect(f).to be_stable
    expect(f.build).to be_a(BuildOptions)
    expect(f.stable.version).to eq(Version.new("0.1"))
    expect(f.head.version).to eq(Version.new("HEAD"))
  end

  specify "#active_spec=" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo"
      version "1.0"
      revision 1
    end

    expect(f.active_spec_sym).to eq(:stable)
    expect(f.active_spec).to eq(f.stable)
    expect(f.pkg_version.to_s).to eq("1.0_1")

    expect { f.active_spec = :head }.to raise_error(FormulaSpecificationError)
  end

  specify "class specs are always initialized" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"
    end

    expect(f.class.stable).to be_a(SoftwareSpec)
    expect(f.class.head).to be_a(SoftwareSpec)
  end

  specify "instance specs have different references" do
    f = Testball.new
    f2 = Testball.new

    expect(f.stable.owner).to equal(f)
    expect(f2.stable.owner).to equal(f2)
  end

  specify "incomplete instance specs are not accessible" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"
    end

    expect(f.head).to be_nil
  end

  describe "#ensure_installed!" do
    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.2.3"
      end
    end

    let(:executable) { Pathname.new("/usr/bin/foo") }

    it "uses a system executable without checking the version by default" do
      allow(f).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(executable)

      expect(SystemCommand).not_to receive(:run)
      expect(f).not_to receive(:any_version_installed?)

      expect(f.ensure_installed!(executable: "foo", output_to_stderr: false)).to eq(executable)
    end

    it "uses a matching system executable when latest is requested" do
      allow(f).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(executable)
      allow(SystemCommand).to receive(:run)
        .with(executable, args: ["--version"], print_stderr: false)
        .and_return(instance_double(SystemCommand::Result, success?: true, stdout: "foo 1.2.3\n"))

      expect(f.ensure_installed!(executable: "foo", latest: true, output_to_stderr: false)).to eq(executable)
    end

    it "passes custom version arguments to the version check" do
      allow(f).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(executable)
      allow(SystemCommand).to receive(:run)
        .with(executable, args: ["-version"], print_stderr: false)
        .and_return(instance_double(SystemCommand::Result, success?: true, stdout: "1.2.3\n"))

      expect(f.ensure_installed!(executable: "foo", latest: true, output_to_stderr: false,
                                 version_args: ["-version"])).to eq(executable)
    end

    it "returns the brewed executable path when the system version does not match latest" do
      allow(f).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(executable)
      allow(SystemCommand).to receive(:run)
        .with(executable, args: ["--version"], print_stderr: false)
        .and_return(instance_double(SystemCommand::Result, success?: true, stdout: "foo 1.2.2\n"))
      allow(f).to receive_messages(any_version_installed?: true, latest_version_installed?: true)

      expect(f.ensure_installed!(executable: "foo", latest: true, output_to_stderr: false)).to eq(f.opt_bin/"foo")
    end
  end

  it "honors attributes declared before specs" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"

      depends_on "foo"
    end

    expect(f.class.stable.deps.first.name).to eq("foo")
    expect(f.class.head.deps.first.name).to eq("foo")
  end

  describe "#pkg_version" do
    specify "simple version" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0.bar"
      end

      expect(f.pkg_version).to eq(PkgVersion.parse("1.0"))
    end

    specify "version with revision" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0.bar"
        revision 1
      end

      expect(f.pkg_version).to eq(PkgVersion.parse("1.0_1"))
    end

    specify "head uses revisions" do
      f = formula "test", spec: :head do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0.bar"
        revision 1

        head "foo"
      end

      expect(f.pkg_version).to eq(PkgVersion.parse("HEAD_1"))
    end
  end

  specify "#update_head_version" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      head "foo", using: :git
    end

    cached_location = f.head.downloader.cached_location
    cached_location.mkpath
    cached_location.cd do
      FileUtils.touch "LICENSE"

      system("git", "init")
      system("git", "add", "--all")
      system("git", "commit", "-m", "Initial commit")
    end

    f.update_head_version

    expect(f.head.version).to eq(Version.new("HEAD-5658946"))
  end

  specify "#desc" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      desc "a formula"

      url "foo-1.0"
    end

    expect(f.desc).to eq("a formula")
  end

  specify "#post_install_defined?" do
    f1 = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"

      def post_install
        # do nothing
      end
    end

    f2 = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"
    end

    expect(f1).to have_post_install_defined
    expect(f2).not_to have_post_install_defined
  end

  specify "#run_post_install prevents build tools from reading user configuration" do
    env = {}
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"
    end

    allow(Tab).to receive(:for_formula).with(f).and_return(f.build)
    allow(f).to receive(:post_install) { env = ENV.to_hash }
    expect(Dir).to receive(:mktmpdir).with("#{f.name}-postinstall-", HOMEBREW_TEMP).and_call_original

    f.run_post_install

    expect(env).to include(
      "GIT_CONFIG_GLOBAL"     => Utils::Git.no_global_config_file,
      "GIT_TERMINAL_PROMPT"   => "0",
      "GOENV"                 => "off",
      "NPM_CONFIG_USERCONFIG" => File::NULL,
      "PIP_CONFIG_FILE"       => File::NULL,
      "XDG_CONFIG_HOME"       => "#{env.fetch("HOME")}/.config",
    )
  end

  specify "#run_post_install runs install steps before the remaining hook" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"
    end

    allow(Tab).to receive(:for_formula).with(f).and_return(f.build)
    allow(f).to receive_messages(post_install_steps_defined?: true, post_install_defined?: true)
    expect(f).to receive(:run_post_install_steps).ordered
    expect(f).to receive(:post_install).ordered

    f.run_post_install
  end

  specify "#post_install_steps" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"

      post_install_steps do
        mkdir_p "log/foo", base: :var
        touch "foo/marker", base: :var
        move "move-source", "move-target"
        move_contents "children-source", "children-target"
        symlink "move-target", "linked-target", source_base: :relative, remove_on_uninstall: true
      end
    end

    expect(f.post_install_steps).to eq([
      { "type" => "mkdir_p", "path" => { "base" => "var", "path" => "log/foo" } },
      { "type" => "touch", "path" => { "base" => "var", "path" => "foo/marker" } },
      {
        "type"      => "move",
        "source"    => { "base" => "prefix", "path" => "move-source" },
        "target"    => { "base" => "prefix", "path" => "move-target" },
        "overwrite" => true,
      },
      {
        "type"   => "move_contents",
        "source" => { "base" => "prefix", "path" => "children-source" },
        "target" => { "base" => "prefix", "path" => "children-target" },
      },
      {
        "type"      => "symlink",
        "source"    => { "base" => "relative", "path" => "move-target" },
        "target"    => { "base" => "prefix", "path" => "linked-target" },
        "uninstall" => true,
      },
    ])
    expect(f.post_install_steps_defined?).to be(true)
    expect(f.to_hash["post_install_steps"]).to eq(f.post_install_steps)
  end

  specify "#post_install_steps does not default paths to var" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"

      post_install_steps do
        touch "foo/marker"
      end
    end

    expect(f.post_install_steps).to eq([
      { "type" => "touch", "path" => { "path" => "foo/marker" } },
    ])
  end

  specify "#post_install_steps_defined? with an empty block" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"

      # This intentionally declares no steps to test definition tracking.
      # rubocop:disable Lint/EmptyBlock
      post_install_steps do
      end
      # rubocop:enable Lint/EmptyBlock
    end

    expect(f.post_install_steps).to be_empty
    expect(f.post_install_steps_defined?).to be(true)
  end

  specify "#post_install_steps can coexist with #post_install" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"

      # This intentionally declares no steps to test definition tracking.
      # rubocop:disable Lint/EmptyBlock
      post_install_steps do
      end
      # rubocop:enable Lint/EmptyBlock

      def post_install; end
    end

    expect(f.post_install_steps_defined?).to be(true)
    expect(f.post_install_defined?).to be(true)
  end

  specify "#run_post_install_steps uses the versioned prefix" do
    f = formula "post-install-steps-prefix" do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"

      post_install_steps do
        symlink "source", "linked"
      end
    end

    versioned_prefix = f.rack/f.pkg_version.to_s
    FileUtils.rm_f f.opt_prefix
    versioned_prefix.mkpath
    f.opt_prefix.parent.mkpath
    FileUtils.ln_s versioned_prefix, f.opt_prefix

    f.run_post_install_steps

    expect((versioned_prefix/"linked").readlink).to eq(versioned_prefix/"source")
  ensure
    FileUtils.rm_f f.opt_prefix
    FileUtils.rm_rf f.rack
  end

  describe "#install_etc_var" do
    let(:f) do
      formula "config-upgrade" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
        version "2.0"
      end
    end
    let(:config_file) { HOMEBREW_PREFIX/"etc/config-upgrade.conf" }
    let(:default_config_file) { Pathname("#{config_file}.default") }
    let(:old_default_file) { f.rack/"1.0/.bottle/etc/config-upgrade.conf" }
    let(:new_default_file) { f.bottle_prefix/"etc/config-upgrade.conf" }

    before do
      FileUtils.rm_rf f.rack
      FileUtils.rm_f config_file
      FileUtils.rm_f default_config_file

      old_default_file.dirname.mkpath
      old_default_file.write "old\n"
      new_default_file.dirname.mkpath
      new_default_file.write "new\n"
      config_file.dirname.mkpath
    end

    it "replaces config that matches the previous default" do
      config_file.write "old\n"

      f.install_etc_var

      expect([config_file.read, default_config_file.exist?]).to eq(["new\n", false])
    end

    it "writes a default file when the config was modified" do
      config_file.write "custom\n"

      f.install_etc_var

      expect([config_file.read, default_config_file.read]).to eq(["custom\n", "new\n"])
    end

    it "replaces config that matches the previous default when the keg is opt-linked" do
      config_file.write "old\n"
      Keg.new(f.rack/"2.0").optlink

      f.install_etc_var

      expect([config_file.read, default_config_file.exist?]).to eq(["new\n", false])
    end
  end

  specify "test fixtures" do
    f1 = formula do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"
    end

    expect(f1.test_fixtures("foo")).to eq(Pathname.new("#{HOMEBREW_LIBRARY_PATH}/test/support/fixtures/foo"))
  end

  specify "#livecheck" do
    f = formula do
      T.bind(self, T.class_of(Formula))
      url "https://brew.sh/test-1.0.tbz"
      livecheck do
        skip "foo"
        url "https://brew.sh/test/releases"
        regex(/test-v?(\d+(?:\.\d+)+)\.t/i)
      end
    end

    expect(f.livecheck.skip?).to be true
    expect(f.livecheck.skip_msg).to eq("foo")
    expect(f.livecheck.url).to eq("https://brew.sh/test/releases")
    expect(f.livecheck.regex).to eq(/test-v?(\d+(?:\.\d+)+)\.t/i)
  end

  describe "#livecheck_defined?" do
    specify "no `livecheck` block defined" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "https://brew.sh/test-1.0.tbz"
      end

      expect(f.livecheck_defined?).to be false
    end

    specify "`livecheck` block defined" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "https://brew.sh/test-1.0.tbz"
        livecheck do
          regex(/test-v?(\d+(?:\.\d+)+)\.t/i)
        end
      end

      expect(f.livecheck_defined?).to be true
    end

    specify "livecheck references Formula URL" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        homepage "https://brew.sh/test"

        url "https://brew.sh/test-1.0.tbz"
        livecheck do
          url :homepage
          regex(/test-v?(\d+(?:\.\d+)+)\.t/i)
        end
      end

      expect(f.livecheck.url).to eq(:homepage)
    end
  end

  describe "#service" do
    specify "no service defined" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "https://brew.sh/test-1.0.tbz"
      end

      expect(f.service.to_hash).to eq({})
    end

    specify "service complicated" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "https://brew.sh/test-1.0.tbz"

        service do
          T.bind(self, Homebrew::Service)
          run [opt_bin/"beanstalkd"]
          run_type :immediate
          error_log_path var/"log/beanstalkd.error.log"
          log_path var/"log/beanstalkd.log"
          working_dir var
          keep_alive true
        end
      end
      expect(f.service.to_hash.keys)
        .to contain_exactly(:run, :run_type, :error_log_path, :log_path, :working_dir, :keep_alive)
    end

    specify "service uses simple run" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "https://brew.sh/test-1.0.tbz"
        service do
          T.bind(self, Homebrew::Service)
          run opt_bin/"beanstalkd"
        end
      end

      expect(f.service.to_hash.keys).to contain_exactly(:run, :run_type)
    end

    specify "service with only custom names" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "https://brew.sh/test-1.0.tbz"
        service do
          name macos: "custom.macos.beanstalkd", linux: "custom.linux.beanstalkd"
        end
      end

      expect(f.plist_name).to eq("custom.macos.beanstalkd")
      expect(f.service_name).to eq("custom.linux.beanstalkd")
      expect(f.service.to_hash.keys).to contain_exactly(:name)
    end

    specify "service helpers return data" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "https://brew.sh/test-1.0.tbz"
      end

      expect(f.plist_name).to eq("homebrew.mxcl.formula_name")
      expect(f.service_name).to eq("homebrew.formula_name")
      expect(f.launchd_service_path).to eq(HOMEBREW_PREFIX/"opt/formula_name/homebrew.mxcl.formula_name.plist")
      expect(f.systemd_service_path).to eq(HOMEBREW_PREFIX/"opt/formula_name/homebrew.formula_name.service")
      expect(f.systemd_timer_path).to eq(HOMEBREW_PREFIX/"opt/formula_name/homebrew.formula_name.timer")
    end
  end

  specify "dependencies" do
    # don't try to load/fetch gcc/glibc
    allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false)

    f1 = formula "f1" do
      T.bind(self, T.class_of(Formula))
      url "f1-1.0"
    end

    f2 = formula "f2" do
      T.bind(self, T.class_of(Formula))
      url "f2-1.0"
    end

    f3 = formula "f3" do
      T.bind(self, T.class_of(Formula))
      url "f3-1.0"

      depends_on "f1" => :build
      depends_on "f2"
    end

    f4 = formula "f4" do
      T.bind(self, T.class_of(Formula))
      url "f4-1.0"

      depends_on "f1"
    end

    stub_formula_loader(f1)
    stub_formula_loader(f2)
    stub_formula_loader(f3)
    stub_formula_loader(f4)

    f5 = formula "f5" do
      T.bind(self, T.class_of(Formula))
      url "f5-1.0"

      depends_on "f3" => :build
      depends_on "f4"
    end

    expect(f5.deps.map(&:name)).to eq(["f3", "f4"])
    expect(f5.recursive_dependencies.map(&:name)).to eq(%w[f1 f2 f3 f4])
    expect(f5.runtime_dependencies.map(&:name)).to eq(["f1", "f4"])
  end

  describe "#runtime_dependencies" do
    specify "runtime dependencies with optional deps from tap" do
      tap_loader = double

      # don't try to load/fetch gcc/glibc
      allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false)

      allow(tap_loader).to receive(:get_formula).and_raise(RuntimeError, "tried resolving tap formula")
      allow(Formulary).to receive(:loader_for).with("foo/bar/f1", from: nil).and_return(tap_loader)

      f2_path = Tap.fetch("baz", "qux").path/"Formula/f2.rb"
      stub_formula_loader(
        formula("f2", path: f2_path) do
          T.bind(self, T.class_of(Formula))
          url("f2-1.0")
        end,
        "baz/qux/f2",
      )

      f3 = formula "f3" do
        T.bind(self, T.class_of(Formula))
        url "f3-1.0"

        depends_on "foo/bar/f1" => :optional
        depends_on "baz/qux/f2"
      end

      expect(f3.runtime_dependencies.map(&:name)).to eq(["baz/qux/f2"])

      described_class.clear_cache

      f1_path = Tap.fetch("foo", "bar").path/"Formula/f1.rb"
      stub_formula_loader(
        formula("f1", path: f1_path) do
          T.bind(self, T.class_of(Formula))
          url("f1-1.0")
        end,
        "foo/bar/f1",
      )

      f3.build = BuildOptions.new(Options.create(["--with-f1"]), f3.options)

      expect(f3.runtime_dependencies.map(&:name)).to eq(["foo/bar/f1", "baz/qux/f2"])
    end

    it "includes non-declared direct dependencies" do
      formula = Class.new(Testball).new
      dependency = formula("dependency") do
        T.bind(self, T.class_of(Formula))
        url "f-1.0"
      end

      formula.brew { formula.install }
      keg = Keg.for(formula.latest_installed_prefix)
      keg.link

      linkage_checker = instance_double(LinkageChecker, "linkage checker", undeclared_deps: [dependency.name])
      allow(LinkageChecker).to receive(:new).and_return(linkage_checker)

      expect(formula.runtime_dependencies.map(&:name)).to eq [dependency.name]
    end

    it "handles bad tab runtime_dependencies" do
      formula = Class.new(Testball).new

      formula.brew { formula.install }
      tab = Tab.create(formula, DevelopmentTools.default_compiler, :libcxx)
      tab.runtime_dependencies = ["foo"]
      tab.write

      keg = Keg.for(formula.latest_installed_prefix)
      keg.link

      expect(formula.runtime_dependencies.map(&:name)).to be_empty
    end
  end

  describe "#missing_dependencies" do
    let(:f) do
      formula("foo") do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end
    let(:keg) { instance_double(Keg) }

    before do
      allow(f).to receive(:any_installed_keg).and_return(keg)
    end

    it "returns empty when no tab runtime_dependencies data" do
      allow(keg).to receive(:runtime_dependencies).and_return(nil)
      expect(f.missing_dependencies).to be_empty
    end

    it "returns empty when dep is present in cellar" do
      (HOMEBREW_CELLAR/"bar").mkpath
      allow(keg).to receive(:runtime_dependencies).and_return([{ "full_name" => "bar" }])
      expect(f.missing_dependencies).to be_empty
    end

    it "returns empty when dep is present as alias or oldname" do
      (HOMEBREW_CELLAR/"bar@2/2.0").mkpath
      (HOMEBREW_PREFIX/"opt").mkpath
      FileUtils.ln_sf HOMEBREW_CELLAR/"bar@2/2.0", HOMEBREW_PREFIX/"opt/bar"
      allow(keg).to receive(:runtime_dependencies).and_return([{ "full_name" => "bar" }])
      expect(f.missing_dependencies).to be_empty
    end

    it "returns dep when not present in cellar" do
      allow(keg).to receive(:runtime_dependencies).and_return([{ "full_name" => "baz" }])
      expect(f.missing_dependencies.map(&:name)).to eq(["baz"])
    end

    it "returns dep as missing when it is in the hide list, even if installed" do
      (HOMEBREW_CELLAR/"bar").mkpath
      allow(keg).to receive(:runtime_dependencies).and_return([{ "full_name" => "bar" }])
      expect(f.missing_dependencies(hide: ["bar"]).map(&:name)).to eq(["bar"])
    end

    it "matches tapnamed deps against base-name hide list" do
      (HOMEBREW_CELLAR/"wget").mkpath
      allow(keg).to receive(:runtime_dependencies).and_return([{ "full_name" => "homebrew/core/wget" }])
      expect(f.missing_dependencies(hide: ["wget"]).map(&:name)).to eq(["homebrew/core/wget"])
    end
  end

  describe "#missing_library_linkage" do
    let(:f) do
      formula("foo") do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    it "returns empty when no keg is installed" do
      allow(f).to receive(:any_installed_keg).and_return(nil)
      expect(f.missing_library_linkage).to eq([[], Set.new])
    end

    it "returns only the formula's own and orphan libraries, excluding dependency-owned ones" do
      keg = instance_double(Keg, directory?: true)
      allow(f).to receive(:any_installed_keg).and_return(keg)
      linkage_checker = instance_double(
        LinkageChecker,
        broken_deps:   { "foo" => ["libfoo.1.dylib"], "gmp" => ["libgmp.10.dylib"] },
        broken_dylibs: Set["liborphan.2.dylib"],
      )
      allow(LinkageChecker).to receive(:new).and_return(linkage_checker)
      expect(f.missing_library_linkage.first).to eq(["libfoo.1.dylib", "liborphan.2.dylib"])
    end

    it "returns the dependency names that own missing libraries, excluding the formula itself" do
      keg = instance_double(Keg, directory?: true)
      allow(f).to receive(:any_installed_keg).and_return(keg)
      linkage_checker = instance_double(
        LinkageChecker,
        broken_deps:   { "foo" => ["libfoo.1.dylib"], "gmp" => ["libgmp.10.dylib"] },
        broken_dylibs: Set.new,
      )
      allow(LinkageChecker).to receive(:new).and_return(linkage_checker)
      expect(f.missing_library_linkage.last).to eq(Set["gmp"])
    end
  end

  specify "requirements" do
    # don't try to load/fetch gcc/glibc
    allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false)

    f1 = formula "f1" do
      T.bind(self, T.class_of(Formula))
      url "f1-1"

      depends_on xcode: ["1.0", :optional]
    end
    stub_formula_loader(f1)

    xcode = XcodeRequirement.new(["1.0", :optional])

    expect(Set.new(f1.recursive_requirements)).to eq(Set[])

    f1.build = BuildOptions.new(Options.create(["--with-xcode"]), f1.options)

    expect(Set.new(f1.recursive_requirements)).to eq(Set[xcode])

    f1.build = f1.stable.build
    f2 = formula "f2" do
      T.bind(self, T.class_of(Formula))
      url "f2-1"

      depends_on "f1"
    end

    expect(Set.new(f2.recursive_requirements)).to eq(Set[])
    expect(
      f2.recursive_requirements do
        # do nothing
      end.to_set,
    ).to eq(Set[xcode])

    requirements = f2.recursive_requirements do |_dependent, requirement|
      next Dependable::PRUNE if requirement.is_a?(XcodeRequirement)
    end

    expect(Set.new(requirements)).to eq(Set[])
  end

  specify "#to_hash" do
    f1 = formula "foo" do
      T.bind(self, T.class_of(Formula))
      url "foo-1.0"

      bottle do
        sha256 cellar: :any, Utils::Bottles.tag.to_sym => TEST_SHA256
      end
    end
    stub_formula_loader(f1)

    h = f1.to_hash

    expect(h).to be_a(Hash)
    expect(h["name"]).to eq("foo")
    expect(h["full_name"]).to eq("foo")
    expect(h["tap"]).to eq("homebrew/core")
    expect(h["versions"]["stable"]).to eq("1.0")
    expect(h["versions"]["bottle"]).to be_truthy
    expect(h["patches"]).to eq([])
  end

  describe "#to_hash patches" do
    it "serialises an external patch" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        patch do
          url "https://example.com/foo.diff"
          sha256 TEST_SHA256
        end
      end

      expect(f.to_hash["patches"]).to eq([
        { "strip" => "p1", "url" => "https://example.com/foo.diff", "sha256" => TEST_SHA256 },
      ])
    end

    it "serialises an external patch with apply and directory" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        patch :p0 do
          url "https://example.com/patches.tar.gz"
          sha256 TEST_SHA256
          directory "src"
          apply "fix-a.patch", "fix-b.patch"
        end
      end

      expect(f.to_hash["patches"]).to eq([
        {
          "strip"     => "p0",
          "url"       => "https://example.com/patches.tar.gz",
          "sha256"    => TEST_SHA256,
          "apply"     => ["fix-a.patch", "fix-b.patch"],
          "directory" => "src",
        },
      ])
    end

    it "serialises an embedded DATA patch" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        patch :p1, :DATA
      end

      expect(f.to_hash["patches"]).to eq([{ "strip" => "p1", "data" => true }])
    end

    it "serialises a string patch" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        patch :p2, "--- a\n+++ b\n"
      end

      expect(f.to_hash["patches"]).to eq([{ "strip" => "p2", "data" => true }])
    end

    it "serialises type and explicit resolves on an external patch" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        patch do
          url "https://example.com/foo.diff"
          sha256 TEST_SHA256
          type :cherry_pick
          resolves "CVE-2024-1111", "CVE-2024-2222"
        end
      end

      expect(f.to_hash["patches"]).to eq([
        {
          "strip"    => "p1",
          "url"      => "https://example.com/foo.diff",
          "sha256"   => TEST_SHA256,
          "type"     => "cherry-pick",
          "resolves" => [
            { "type" => "security", "id" => "CVE-2024-1111" },
            { "type" => "security", "id" => "CVE-2024-2222" },
          ],
        },
      ])
    end

    it "serialises resolves inferred from url and apply paths" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        patch do
          url "https://example.com/debian.tar.xz"
          sha256 TEST_SHA256
          apply "patches/CVE-2024-1234.patch", "patches/cve-2024-5678.patch"
        end
      end

      expect(f.to_hash["patches"].first["resolves"]).to eq([
        { "type" => "security", "id" => "CVE-2024-1234" },
        { "type" => "security", "id" => "CVE-2024-5678" },
      ])
    end

    it "serialises non-CVE resolves entries with the appropriate issue type" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        patch do
          url "https://example.com/foo.diff"
          sha256 TEST_SHA256
          resolves "CVE-2024-1234", "GHSA-xr7r-f8xq-vfvv", "https://github.com/foo/bar/issues/1"
        end
      end

      expect(f.to_hash["patches"].first["resolves"]).to eq([
        { "type" => "security", "id" => "CVE-2024-1234" },
        { "type" => "security", "id" => "GHSA-xr7r-f8xq-vfvv" },
        { "type" => "defect", "id" => "https://github.com/foo/bar/issues/1" },
      ])
    end

    it "serialises type on a local file patch" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        patch do
          file "Patches/foo.diff"
          type :unofficial
        end
      end

      expect(f.to_hash["patches"]).to eq([{ "strip" => "p1", "file" => "Patches/foo.diff", "type" => "unofficial" }])
    end

    it "serialises a local file patch" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        patch do
          file "Patches/foo.diff"
        end
      end

      expect(f.to_hash["patches"]).to eq([{ "strip" => "p1", "file" => "Patches/foo.diff" }])
    end
  end

  describe "#to_hash_with_variations", :needs_macos do
    let(:formula_path) { CoreTap.instance.new_formula_path("foo-variations") }
    let(:formula_content) do
      <<~RUBY
        class FooVariations < Formula
          url "file://#{TEST_FIXTURE_DIR}/tarballs/testball-0.1.tbz"
          sha256 TESTBALL_SHA256

          on_intel do
            depends_on "intel-formula"
          end

          on_sequoia do
            depends_on "sequoia-formula"
          end

          on_sonoma :or_older do
            depends_on "sonoma-or-older-formula"
          end

          on_linux do
            depends_on "linux-formula"
          end
        end
      RUBY
    end
    let(:expected_variations) do
      <<~JSON
        {
          "tahoe": {
            "dependencies": [
              "intel-formula"
            ]
          },
          "arm64_tahoe": {
            "dependencies": []
          },
          "sequoia": {
            "dependencies": [
              "intel-formula",
              "sequoia-formula"
            ]
          },
          "arm64_sequoia": {
            "dependencies": [
              "sequoia-formula"
            ]
          },
          "sonoma": {
            "dependencies": [
              "intel-formula",
              "sonoma-or-older-formula"
            ]
          },
          "ventura": {
            "dependencies": [
              "intel-formula",
              "sonoma-or-older-formula"
            ]
          },
          "x86_64_linux": {
            "dependencies": [
              "intel-formula",
              "linux-formula"
            ]
          },
          "arm64_linux": {
            "dependencies": [
              "linux-formula"
            ]
          }
        }
      JSON
    end

    before do
      # Use a more limited os list to shorten the variations hash
      os_list = [:tahoe, :sequoia, :sonoma, :ventura, :linux]
      valid_tags = os_list.product(OnSystem::ARCH_OPTIONS).filter_map do |os, arch|
        tag = Utils::Bottles::Tag.new(system: os, arch:)
        next unless tag.valid_combination?

        tag
      end
      stub_const("OnSystem::VALID_OS_ARCH_TAGS", valid_tags)

      # For consistency, always run on Tahoe and ARM
      allow(MacOS).to receive(:version).and_return(MacOSVersion.new("12"))
      allow(Hardware::CPU).to receive(:type).and_return(:arm)

      formula_path.dirname.mkpath
      formula_path.write formula_content
    end

    it "returns the correct variations hash" do
      h = Formulary.factory("foo-variations").to_hash_with_variations

      expect(h).to be_a(Hash)
      expect(JSON.pretty_generate(h["variations"])).to eq expected_variations.strip
    end
  end

  describe "#eligible_kegs_for_cleanup" do
    it "returns Kegs eligible for cleanup" do
      f1 = Class.new(Testball) do
        version("1.0")
      end.new

      f2 = Class.new(Testball) do
        version("0.2")
        version_scheme(1)
      end.new

      f3 = Class.new(Testball) do
        version("0.3")
        version_scheme(1)
      end.new

      f4 = Class.new(Testball) do
        version("0.1")
        version_scheme(2)
      end.new

      [f1, f2, f3, f4].each do |f|
        f.brew { f.install }
        Tab.create(f, DevelopmentTools.default_compiler, :libcxx).write
      end

      expect(f1).to be_latest_version_installed
      expect(f2).to be_latest_version_installed
      expect(f3).to be_latest_version_installed
      expect(f4).to be_latest_version_installed
      expect(f3.eligible_kegs_for_cleanup.sort_by(&:version))
        .to eq([f2, f1].map { |f| Keg.new(f.prefix) })
    end

    specify "with pinned Keg" do
      f1 = Class.new(Testball) { version("0.1") }.new
      f2 = Class.new(Testball) { version("0.2") }.new
      f3 = Class.new(Testball) { version("0.3") }.new

      f1.brew { f1.install }
      f1.pin
      f2.brew { f2.install }
      f3.brew { f3.install }

      expect(f1.prefix).to eq((HOMEBREW_PINNED_KEGS/f1.name).resolved_path)
      expect(f1).to be_latest_version_installed
      expect(f2).to be_latest_version_installed
      expect(f3).to be_latest_version_installed
      expect(f3.eligible_kegs_for_cleanup).to eq([Keg.new(f2.prefix)])
    end

    specify "with HEAD installed" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        version("0.1")
        head("foo")
      end

      ["0.0.1", "0.0.2", "0.1", "HEAD-000000", "HEAD-111111", "HEAD-111111_1"].each do |version|
        prefix = f.prefix(version)
        prefix.mkpath
        tab = Tab.empty
        tab.tabfile = prefix/AbstractTab::FILENAME
        tab.source_modified_time = 1
        tab.write
      end

      eligible_kegs = f.installed_kegs - [Keg.new(f.prefix("HEAD-111111_1")), Keg.new(f.prefix("0.1"))]
      expect(f.eligible_kegs_for_cleanup.sort_by(&:version)).to eq(eligible_kegs.sort_by(&:version))
    end
  end

  describe "#pour_bottle?" do
    it "returns false if set to false" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"

        def pour_bottle?
          false
        end
      end

      expect(f).not_to pour_bottle
    end

    it "returns true if set to true" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"

        def pour_bottle?
          true
        end
      end

      expect(f).to pour_bottle
    end

    it "returns false if set to false via DSL" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"

        pour_bottle? do
          reason "false reason"
          satisfy { var == etc }
        end
      end

      expect(f).not_to pour_bottle
    end

    it "returns true if set to true via DSL" do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"

        pour_bottle? do
          reason "true reason"
          satisfy { true }
        end
      end

      expect(f).to pour_bottle
    end

    it "returns false with `only_if: :clt_installed` on macOS", :needs_macos do
      # Pretend CLT is not installed
      allow(MacOS::CLT).to receive(:installed?).and_return(false)

      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"

        pour_bottle? only_if: :clt_installed
      end

      expect(f).not_to pour_bottle
    end

    it "returns true with `only_if: :clt_installed` on macOS", :needs_macos do
      # Pretend CLT is installed
      allow(MacOS::CLT).to receive(:installed?).and_return(true)

      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"

        pour_bottle? only_if: :clt_installed
      end

      expect(f).to pour_bottle
    end

    it "returns true with `only_if: :clt_installed` on Linux", :needs_linux do
      f = formula "foo" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"

        pour_bottle? only_if: :clt_installed
      end

      expect(f).to pour_bottle
    end

    it "throws an error if passed both a symbol and a block" do
      expect do
        formula "foo" do
          T.bind(self, T.class_of(Formula))
          url "foo-1.0"

          pour_bottle? only_if: :clt_installed do
            reason "true reason"
            satisfy { true }
          end
        end
      end.to raise_error(ArgumentError, "Do not pass both a preset condition and a block to `pour_bottle?`")
    end

    it "throws an error if passed an invalid symbol" do
      expect do
        formula "foo" do
          T.bind(self, T.class_of(Formula))
          url "foo-1.0"

          pour_bottle? only_if: :foo
        end
      end.to raise_error(ArgumentError, "Invalid preset `pour_bottle?` condition")
    end
  end

  describe "alias changes" do
    let(:f) do
      formula("formula_name", alias_path:) do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    let(:new_formula) do
      formula("new_formula_name", alias_path:) do
        T.bind(self, T.class_of(Formula))
        url "foo-1.1"
      end
    end

    let(:tab) { Tab.empty }
    let(:alias_name) { "bar" }
    let(:alias_path) { CoreTap.instance.alias_dir/alias_name }

    before do
      stub_formula_loader(f)
      stub_formula_loader(new_formula)
      allow(described_class).to receive(:installed).and_return([f])

      f.build = tab
      new_formula.build = tab
    end

    specify "alias changes when not installed with alias" do
      tab.source["path"] = Formulary.core_path(f.name).to_s

      expect(f.current_installed_alias_target).to be_nil
      expect(f.latest_formula).to eq(f)
      expect(f).not_to have_changed_installed_alias_target
      expect(f).not_to supersede_an_installed_formula
      expect(f).not_to have_changed_alias
      expect(f.old_installed_formulae).to be_empty
    end

    specify "alias changes when not changed" do
      tab.source["path"] = alias_path.to_s
      stub_formula_loader(f, alias_name)

      CoreTap.instance.alias_dir.mkpath
      FileUtils.ln_sf f.path, alias_path

      expect(f.current_installed_alias_target).to eq(f)
      expect(f.latest_formula).to eq(f)
      expect(f).not_to have_changed_installed_alias_target
      expect(f).not_to supersede_an_installed_formula
      expect(f).not_to have_changed_alias
      expect(f.old_installed_formulae).to be_empty
    end

    specify "alias changes when new alias target" do
      tab.source["path"] = alias_path.to_s
      stub_formula_loader(new_formula, alias_name)

      CoreTap.instance.alias_dir.mkpath
      FileUtils.ln_sf new_formula.path, alias_path

      expect(f.current_installed_alias_target).to eq(new_formula)
      expect(f.latest_formula).to eq(new_formula)
      expect(f).to have_changed_installed_alias_target
      expect(f).not_to supersede_an_installed_formula
      expect(f).to have_changed_alias
      expect(f.old_installed_formulae).to be_empty
    end

    specify "alias changes when old formulae installed" do
      tab.source["path"] = alias_path.to_s
      stub_formula_loader(new_formula, alias_name)

      CoreTap.instance.alias_dir.mkpath
      FileUtils.ln_sf new_formula.path, alias_path

      expect(new_formula.current_installed_alias_target).to eq(new_formula)
      expect(new_formula.latest_formula).to eq(new_formula)
      expect(new_formula).not_to have_changed_installed_alias_target
      expect(new_formula).to supersede_an_installed_formula
      expect(new_formula).to have_changed_alias
      expect(new_formula.old_installed_formulae).to eq([f])
    end
  end

  describe "#outdated_kegs" do
    let(:outdated_prefix) { HOMEBREW_CELLAR/"#{f.name}/1.11" }
    let(:same_prefix) { HOMEBREW_CELLAR/"#{f.name}/1.20" }
    let(:greater_prefix) { HOMEBREW_CELLAR/"#{f.name}/1.21" }
    let(:head_prefix) { HOMEBREW_CELLAR/"#{f.name}/HEAD" }
    let(:old_alias_target_prefix) { HOMEBREW_CELLAR/"#{old_formula.name}/1.0" }

    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo"
        version "1.20"
      end
    end

    let(:old_formula) do
      formula "foo@1" do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    let(:new_formula) do
      formula "foo@2" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end
    end

    let(:alias_name) { "bar" }
    let(:alias_path) { f.tap.alias_dir/alias_name }

    before do
      stub_formula_loader(f)
      stub_formula_loader(old_formula)
      stub_formula_loader(new_formula)
    end

    def setup_tab_for_prefix(prefix, options = {})
      prefix.mkpath

      keg = Keg.new(prefix)
      keg.optlink

      tab = Tab.empty
      tab.tabfile = prefix/AbstractTab::FILENAME
      tab.source["path"] = options[:path].to_s if options[:path]
      tab.source["tap"] = options[:tap] if options[:tap]
      tab.source["versions"] = options[:versions] if options[:versions]
      tab.source_modified_time = options[:source_modified_time].to_i
      tab.write unless options[:no_write]
      tab
    end

    example "greater different tap installed" do
      setup_tab_for_prefix(greater_prefix, tap: "user/repo")
      expect(f.outdated_kegs).to be_empty
    end

    example "greater same tap installed" do
      f.tap = CoreTap.instance
      setup_tab_for_prefix(greater_prefix, tap: "homebrew/core")
      expect(f.outdated_kegs).to be_empty
    end

    example "outdated different tap installed" do
      setup_tab_for_prefix(outdated_prefix, tap: "user/repo")
      expect(f.outdated_kegs).not_to be_empty
    end

    example "outdated same tap installed" do
      f.tap = CoreTap.instance
      setup_tab_for_prefix(outdated_prefix, tap: "homebrew/core")
      expect(f.outdated_kegs).not_to be_empty
    end

    example "outdated unlinked tap installed" do
      setup_tab_for_prefix(same_prefix)
      Keg.new(same_prefix).remove_opt_record
      expect(f.outdated_kegs).not_to be_empty
    end

    example "outdated follow alias and alias unchanged" do
      f.follow_installed_alias = true
      f.build = setup_tab_for_prefix(same_prefix, path: alias_path)
      stub_formula_loader(f, alias_name)
      expect(f.outdated_kegs).to be_empty
    end

    example "outdated follow alias and alias changed and new target not installed" do
      f.follow_installed_alias = true
      f.build = setup_tab_for_prefix(same_prefix, path: alias_path)
      stub_formula_loader(new_formula, alias_name)

      CoreTap.instance.alias_dir.mkpath
      FileUtils.ln_sf new_formula.path, alias_path

      expect(f.outdated_kegs).not_to be_empty
    end

    example "outdated follow alias and alias changed and new target installed" do
      f.follow_installed_alias = true
      f.build = setup_tab_for_prefix(same_prefix, path: alias_path)
      stub_formula_loader(new_formula, alias_name)
      setup_tab_for_prefix(new_formula.prefix)
      expect(f.outdated_kegs).to be_empty
    end

    example "outdated no follow alias and alias unchanged" do
      f.follow_installed_alias = false
      f.build = setup_tab_for_prefix(same_prefix, path: alias_path)
      stub_formula_loader(f, alias_name)
      expect(f.outdated_kegs).to be_empty
    end

    example "outdated no follow alias and alias changed" do
      f.follow_installed_alias = false
      f.build = setup_tab_for_prefix(same_prefix, path: alias_path)

      f2 = formula "foo@2" do
        T.bind(self, T.class_of(Formula))
        url "foo-2.0"
      end

      stub_formula_loader(f2, alias_path)
      expect(f.outdated_kegs).to be_empty
    end

    example "outdated old alias targets installed" do
      f = formula(alias_path:) do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end

      tab = setup_tab_for_prefix(old_alias_target_prefix, path: alias_path)
      old_formula.build = tab
      allow(described_class).to receive(:installed).and_return([old_formula])

      CoreTap.instance.alias_dir.mkpath
      FileUtils.ln_sf f.path, alias_path

      expect(f.outdated_kegs).not_to be_empty
    end

    example "outdated old alias targets not installed" do
      f = formula(alias_path:) do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end

      tab = setup_tab_for_prefix(old_alias_target_prefix, path: old_formula.path)
      old_formula.build = tab
      allow(described_class).to receive(:installed).and_return([old_formula])
      expect(f.outdated_kegs).to be_empty
    end

    example "outdated same head installed" do
      f.tap = CoreTap.instance
      setup_tab_for_prefix(head_prefix, tap: "homebrew/core")
      expect(f.outdated_kegs).to be_empty
    end

    example "outdated different head installed" do
      f.tap = CoreTap.instance
      setup_tab_for_prefix(head_prefix, tap: "user/repo")
      expect(f.outdated_kegs).to be_empty
    end

    example "outdated mixed taps greater version installed" do
      f.tap = CoreTap.instance
      setup_tab_for_prefix(outdated_prefix, tap: "homebrew/core")
      setup_tab_for_prefix(greater_prefix, tap: "user/repo")

      expect(f.outdated_kegs).to be_empty

      setup_tab_for_prefix(greater_prefix, tap: "homebrew/core")
      described_class.clear_cache

      expect(f.outdated_kegs).to be_empty
    end

    example "outdated mixed taps outdated version installed" do
      f.tap = CoreTap.instance

      extra_outdated_prefix = HOMEBREW_CELLAR/f.name/"1.0"

      setup_tab_for_prefix(outdated_prefix)
      setup_tab_for_prefix(extra_outdated_prefix, tap: "homebrew/core")
      described_class.clear_cache

      expect(f.outdated_kegs).not_to be_empty

      setup_tab_for_prefix(outdated_prefix, tap: "user/repo")
      described_class.clear_cache

      expect(f.outdated_kegs).not_to be_empty
    end

    example "outdated same version tap installed" do
      f.tap = CoreTap.instance
      setup_tab_for_prefix(same_prefix, tap: "homebrew/core")

      expect(f.outdated_kegs).to be_empty

      setup_tab_for_prefix(same_prefix, tap: "user/repo")
      described_class.clear_cache

      expect(f.outdated_kegs).to be_empty
    end

    example "outdated installed head less than stable" do
      tab = setup_tab_for_prefix(head_prefix, versions: { "stable" => "1.0" })

      expect(f.outdated_kegs).not_to be_empty

      tab.source["versions"] = { "stable" => f.version.to_s }
      tab.write
      described_class.clear_cache

      expect(f.outdated_kegs).to be_empty
    end

    describe ":fetch_head" do
      let(:f) do
        repo = testball_repo
        formula "testball" do
          T.bind(self, T.class_of(Formula))
          url "foo"
          version "2.10"
          head "file://#{repo}", using: :git
        end
      end
      let(:testball_repo) { HOMEBREW_PREFIX/"testball_repo" }

      example do
        outdated_stable_prefix = HOMEBREW_CELLAR/"testball/1.0"
        head_prefix_a = HOMEBREW_CELLAR/"testball/HEAD"
        head_prefix_b = HOMEBREW_CELLAR/"testball/HEAD-aaaaaaa_1"
        head_prefix_c = HOMEBREW_CELLAR/"testball/HEAD-18a7103"

        setup_tab_for_prefix(outdated_stable_prefix)
        tab_a = setup_tab_for_prefix(head_prefix_a, versions: { "stable" => "1.0" })
        setup_tab_for_prefix(head_prefix_b)

        testball_repo.mkdir
        testball_repo.cd do
          FileUtils.touch "LICENSE"

          system("git", "-c", "init.defaultBranch=master", "init")
          system("git", "add", "--all")
          system("git", "commit", "-m", "Initial commit")
        end

        expect(f.outdated_kegs(fetch_head: true)).not_to be_empty

        tab_a.source["versions"] = { "stable" => f.version.to_s }
        tab_a.write
        described_class.clear_cache
        expect(f.outdated_kegs(fetch_head: true)).not_to be_empty

        FileUtils.rm_r(head_prefix_a)
        described_class.clear_cache
        expect(f.outdated_kegs(fetch_head: true)).not_to be_empty

        setup_tab_for_prefix(head_prefix_c, source_modified_time: 1)
        described_class.clear_cache
        expect(f.outdated_kegs(fetch_head: true)).to be_empty
      ensure
        FileUtils.rm_r(testball_repo) if testball_repo.exist?
      end
    end

    describe "#mkdir" do
      let(:dst) { mktmpdir }

      it "creates intermediate directories" do
        f.mkdir dst/"foo/bar/baz" do
          expect(dst/"foo/bar/baz").to exist, "foo/bar/baz was not created"
          expect(dst/"foo/bar/baz").to be_a_directory, "foo/bar/baz was not a directory structure"
        end
      end
    end

    describe "with changed version scheme" do
      let(:f) do
        formula "testball" do
          T.bind(self, T.class_of(Formula))
          url "foo"
          version "20141010"
          version_scheme 1
        end
      end

      example do
        prefix = HOMEBREW_CELLAR/"testball/0.1"
        setup_tab_for_prefix(prefix, versions: { "stable" => "0.1" })

        expect(f.outdated_kegs).not_to be_empty
      end
    end

    describe "with mixed version schemes" do
      let(:f) do
        formula "testball" do
          T.bind(self, T.class_of(Formula))
          url "foo"
          version "20141010"
          version_scheme 3
        end
      end

      example do
        prefix_a = HOMEBREW_CELLAR/"testball/20141009"
        setup_tab_for_prefix(prefix_a, versions: { "stable" => "20141009", "version_scheme" => 1 })

        prefix_b = HOMEBREW_CELLAR/"testball/2.14"
        setup_tab_for_prefix(prefix_b, versions: { "stable" => "2.14", "version_scheme" => 2 })

        expect(f.outdated_kegs).not_to be_empty
        described_class.clear_cache

        prefix_c = HOMEBREW_CELLAR/"testball/20141009"
        setup_tab_for_prefix(prefix_c, versions: { "stable" => "20141009", "version_scheme" => 3 })

        expect(f.outdated_kegs).not_to be_empty
        described_class.clear_cache

        prefix_d = HOMEBREW_CELLAR/"testball/20141011"
        setup_tab_for_prefix(prefix_d, versions: { "stable" => "20141009", "version_scheme" => 3 })
        expect(f.outdated_kegs).to be_empty
      end
    end

    describe "with version scheme" do
      let(:f) do
        formula "testball" do
          T.bind(self, T.class_of(Formula))
          url "foo"
          version "1.0"
          version_scheme 2
        end
      end

      example do
        head_prefix = HOMEBREW_CELLAR/"testball/HEAD"

        setup_tab_for_prefix(head_prefix, versions: { "stable" => "1.0", "version_scheme" => 1 })
        expect(f.outdated_kegs).not_to be_empty

        described_class.clear_cache
        FileUtils.rm_r(head_prefix)

        setup_tab_for_prefix(head_prefix, versions: { "stable" => "1.0", "version_scheme" => 2 })
        expect(f.outdated_kegs).to be_empty
      end
    end
  end

  describe "#any_installed_version" do
    let(:f) do
      Class.new(Testball) do
        version "1.0"
        revision 1
      end.new
    end

    it "returns nil when not installed" do
      expect(f.any_installed_version).to be_nil
    end

    it "returns package version when installed" do
      f.brew { f.install }
      expect(f.any_installed_version).to eq(PkgVersion.parse("1.0_1"))
    end
  end

  describe "OS support" do
    it "returns false for Linux when macOS is required at the top level" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "foo"
        version "1.0"
        depends_on macos: :catalina
      end

      expect(f.supports_linux?).to be false
    end

    it "returns true for Linux when macOS is required in an on_macos block" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "foo"
        version "1.0"
        on_macos do
          depends_on macos: :catalina
        end
      end

      expect(f.supports_linux?).to be true
    end

    it "returns false for macOS when Linux is required at the top level" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "foo"
        version "1.0"
        depends_on :linux
      end

      expect(f.supports_macos?).to be false
      expect(f.supports_linux?).to be true
    end

    it "deprecates bare and versioned macOS requirements" do
      expect do
        formula do
          T.bind(self, T.class_of(Formula))
          url "foo"
          version "1.0"
          depends_on :macos
          depends_on macos: :catalina
        end
      end.to raise_error(MethodDeprecatedError,
                         /`depends_on :macos` with `depends_on macos:` inside an `on_macos` block/)
    end

    it "does not allow duplicate bare macOS requirements" do
      expect do
        formula do
          T.bind(self, T.class_of(Formula))
          url "foo"
          version "1.0"
          depends_on :macos
          depends_on :macos
        end
      end.to raise_error(ArgumentError, "`depends_on :macos` cannot be combined with another macOS `depends_on`")
    end

    it "returns false for Linux when maximum macOS is required at the top level" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "foo"
        version "1.0"
        depends_on maximum_macos: :tahoe
      end

      expect(f.supports_linux?).to be false
    end

    it "does not allow Linux then macOS requirements" do
      expect do
        formula do
          T.bind(self, T.class_of(Formula))
          url "foo"
          version "1.0"
          depends_on :linux
          depends_on macos: :catalina
        end
      end.to raise_error(ArgumentError, "`depends_on :linux` cannot be combined with `depends_on macos:`")
    end

    it "does not allow macOS then Linux requirements" do
      expect do
        formula do
          T.bind(self, T.class_of(Formula))
          url "foo"
          version "1.0"
          depends_on macos: :catalina
          depends_on :linux
        end
      end.to raise_error(ArgumentError, "`depends_on :linux` cannot be combined with `depends_on macos:`")
    end
  end

  describe "#on_macos", :needs_macos do
    let(:f) do
      Class.new(Testball) do
        attr_reader :test

        def install
          @test = 0
          on_macos do
            @test = 1
          end
          on_linux do
            @test = 2
          end
        end
      end.new
    end

    it "only calls code within on_macos" do
      f.brew { f.install }
      expect(f.test).to eq(1)
    end
  end

  describe "#on_linux", :needs_linux do
    let(:f) do
      Class.new(Testball) do
        attr_reader :test

        def install
          @test = 0
          on_macos do
            @test = 1
          end
          on_linux do
            @test = 2
          end
        end
      end.new
    end

    it "only calls code within on_linux" do
      f.brew { f.install }
      expect(f.test).to eq(2)
    end
  end

  describe "#on_system" do
    let(:f) do
      Class.new(Testball) do
        attr_reader :foo
        attr_reader :bar

        def install
          @foo = 0
          @bar = 0
          on_system :linux, macos: :tahoe do
            @foo = 1
          end
          on_system :linux, macos: :sonoma_or_older do
            @bar = 1
          end
        end
      end.new
    end

    it "doesn't call code on Sequoia", :needs_macos do
      Homebrew::SimulateSystem.with os: :sequoia do
        f.brew { f.install }
        expect(f.foo).to eq(0)
        expect(f.bar).to eq(0)
      end
    end

    it "calls code on Linux", :needs_linux do
      Homebrew::SimulateSystem.with os: :linux do
        f.brew { f.install }
        expect(f.foo).to eq(1)
        expect(f.bar).to eq(1)
      end
    end

    it "calls code within `on_system :linux, macos: :tahoe` on Tahoe", :needs_macos do
      Homebrew::SimulateSystem.with os: :tahoe do
        f.brew { f.install }
        expect(f.foo).to eq(1)
        expect(f.bar).to eq(0)
      end
    end

    it "calls code within `on_system :linux, macos: :sonoma_or_older` on Sonoma", :needs_macos do
      Homebrew::SimulateSystem.with os: :sonoma do
        f.brew { f.install }
        expect(f.foo).to eq(0)
        expect(f.bar).to eq(1)
      end
    end

    it "calls code within `on_system :linux, macos: :sonoma_or_older` on Ventura", :needs_macos do
      Homebrew::SimulateSystem.with os: :ventura do
        f.brew { f.install }
        expect(f.foo).to eq(0)
        expect(f.bar).to eq(1)
      end
    end
  end

  describe "on_{os_version} blocks", :needs_macos do
    let(:f) do
      Class.new(Testball) do
        attr_reader :test

        def install
          @test = 0
          on_sequoia :or_newer do
            @test = 1
          end
          on_sonoma do
            @test = 2
          end
          on_ventura :or_older do
            @test = 3
          end
        end
      end.new
    end

    it "only calls code within `on_sequoia`" do
      Homebrew::SimulateSystem.with os: :tahoe do
        f.brew { f.install }
        expect(f.test).to eq(1)
      end
    end

    it "only calls code within `on_sequoia :or_newer`" do
      Homebrew::SimulateSystem.with os: :sequoia do
        f.brew { f.install }
        expect(f.test).to eq(1)
      end
    end

    it "only calls code within `on_sonoma`" do
      Homebrew::SimulateSystem.with os: :sonoma do
        f.brew { f.install }
        expect(f.test).to eq(2)
      end
    end

    it "only calls code within `on_ventura`" do
      Homebrew::SimulateSystem.with os: :ventura do
        f.brew { f.install }
        expect(f.test).to eq(3)
      end
    end

    it "only calls code within `on_ventura :or_older`" do
      Homebrew::SimulateSystem.with os: :monterey do
        f.brew { f.install }
        expect(f.test).to eq(3)
      end
    end
  end

  describe "#on_arm" do
    before do
      allow(Hardware::CPU).to receive(:type).and_return(:arm)
    end

    let(:f) do
      Class.new(Testball) do
        attr_reader :test

        def install
          @test = 0
          on_arm do
            @test = 1
          end
          on_intel do
            @test = 2
          end
        end
      end.new
    end

    it "only calls code within on_arm" do
      f.brew { f.install }
      expect(f.test).to eq(1)
    end
  end

  describe "#on_intel" do
    before do
      allow(Hardware::CPU).to receive(:type).and_return(:intel)
    end

    let(:f) do
      Class.new(Testball) do
        attr_reader :test

        def install
          @test = 0
          on_arm do
            @test = 1
          end
          on_intel do
            @test = 2
          end
        end
      end.new
    end

    it "only calls code within on_intel" do
      f.brew { f.install }
      expect(f.test).to eq(2)
    end
  end

  describe "#generate_completions_from_executable" do
    let(:f) do
      Class.new(Testball) do
        def install
          bin.mkpath
          (bin/"foo").write <<-EOF
            echo completion
          EOF

          FileUtils.chmod "+x", bin/"foo"

          generate_completions_from_executable(bin/"foo", "test")
        end
      end.new
    end

    it "generates completion scripts" do
      f.brew { f.install }
      expect(f.bash_completion/"foo").to be_a_file
      expect(f.zsh_completion/"_foo").to be_a_file
      expect(f.fish_completion/"foo.fish").to be_a_file
    end
  end

  describe "{allow,deny}_network_access" do
    actions = %w[allow deny].freeze
    PHASES.each do |phase|
      actions.each do |action|
        it "can #{action} network access for #{phase}" do
          f = Class.new(Testball) do
            public_send(:"#{action}_network_access!", phase)
          end

          expect(f.network_access_allowed?(phase)).to be(action == "allow")
        end
      end
    end

    test_each(actions) do |action|
      it "can #{action} network access for all phases" do
        f = Class.new(Testball) do
          public_send(:"#{action}_network_access!")
        end

        PHASES.each do |phase|
          expect(f.network_access_allowed?(phase)).to be(action == "allow")
        end
      end
    end
  end

  describe "#network_access_allowed?" do
    it "throws an error when passed an invalid symbol" do
      f = Testball.new
      expect { f.network_access_allowed?(:foo) }.to raise_error(ArgumentError)
    end
  end

  describe "#specified_path" do
    let(:klass) do
      Class.new(described_class) do
        url "https://brew.sh/foo-1.0.tar.gz"
      end
    end

    let(:name) { "formula_name" }
    let(:path) { Formulary.core_path(name) }
    let(:spec) { :stable }
    let(:alias_name) { "baz@1" }
    let(:alias_path) { CoreTap.instance.alias_dir/alias_name }
    let(:f) { klass.new(name, path, spec) }
    let(:f_alias) { klass.new(name, path, spec, alias_path:) }

    context "when loading from a formula file" do
      it "returns the formula file path" do
        expect(f.specified_path).to eq(path)
      end
    end

    context "when loaded from an alias" do
      it "returns the alias path" do
        expect(f_alias.specified_path).to eq(alias_path)
      end
    end

    context "when loaded from the API" do
      before do
        allow(f).to receive(:loaded_from_api?).and_return(true)
      end

      it "returns the API path" do
        expect(f.specified_path).to eq(Homebrew::API::Formula.cached_json_file_path)
      end
    end

    context "when loaded from the internal API" do
      before do
        allow(f).to receive(:loaded_from_internal_api?).and_return(true)
      end

      it "returns the internal API path" do
        expect(f.specified_path).to eq(Homebrew::API::Internal.cached_packages_json_file_path)
      end
    end
  end

  describe "#preserve_rpath" do
    it "defaults to false" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end

      expect(f.class.preserve_rpath?).to be(false)
    end

    it "can be enabled" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        preserve_rpath
      end

      expect(f.class.preserve_rpath?).to be(true)
    end

    it "can be explicitly disabled" do
      f = formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
        preserve_rpath value: false
      end

      expect(f.class.preserve_rpath?).to be(false)
    end
  end

  describe "#deprecate! and #disable!" do
    let(:deprecation_date) { "2020-01-01" }
    let(:disable_date) { "2021-01-01" }

    context "with both dates provided in correct order" do
      let(:f) do
        deprecation_date_ = deprecation_date
        disable_date_ = disable_date
        formula "foo" do
          T.bind(self, T.class_of(Formula))
          url "foo-1.0"
          deprecate! date: deprecation_date_.to_s, because: :unmaintained
          disable! date: disable_date_.to_s, because: :unsupported
        end
      end

      it "is not deprecated before deprecation date" do
        allow(Date).to receive(:today).and_return(Date.parse(deprecation_date) - 1)
        expect(f.deprecated?).to be(false)
        expect(f.deprecation_reason).to be_nil
        expect(f.disabled?).to be(false)
        expect(f.disable_reason).to be_nil
      end

      it "is deprecated on deprecation date" do
        allow(Date).to receive(:today).and_return(Date.parse(deprecation_date))
        expect(f.deprecated?).to be(true)
        expect(f.deprecation_reason).to be(:unmaintained)
        expect(f.disabled?).to be(false)
        expect(f.disable_reason).to be_nil
      end

      it "is disabled on disable date" do
        allow(Date).to receive(:today).and_return(Date.parse(disable_date))
        expect(f.deprecated?).to be(true)
        expect(f.deprecation_reason).to be(:unmaintained)
        expect(f.disabled?).to be(true)
        expect(f.disable_reason).to be(:unsupported)
      end
    end

    context "with both dates provided in incorrect order" do
      let(:f) do
        deprecation_date_ = deprecation_date
        disable_date_ = disable_date
        formula "foo" do
          T.bind(self, T.class_of(Formula))
          url "foo-1.0"
          disable! date: disable_date_.to_s, because: :unsupported
          deprecate! date: deprecation_date_.to_s, because: :unmaintained
        end
      end

      it "is not deprecated before deprecation date" do
        allow(Date).to receive(:today).and_return(Date.parse(deprecation_date) - 1)
        expect(f.deprecated?).to be(false)
        expect(f.deprecation_reason).to be_nil
        expect(f.disabled?).to be(false)
        expect(f.disable_reason).to be_nil
      end

      it "is deprecated on deprecation date" do
        allow(Date).to receive(:today).and_return(Date.parse(deprecation_date))
        expect(f.deprecated?).to be(true)
        expect(f.deprecation_reason).to be(:unmaintained)
        expect(f.disabled?).to be(false)
        expect(f.disable_reason).to be_nil
      end

      it "is disabled on disable date" do
        allow(Date).to receive(:today).and_return(Date.parse(disable_date))
        expect(f.deprecated?).to be(true)
        expect(f.deprecation_reason).to be(:unmaintained)
        expect(f.disabled?).to be(true)
        expect(f.disable_reason).to be(:unsupported)
      end
    end

    context "with only disable date" do
      let(:f) do
        disable_date_ = disable_date
        formula("foo") do
          T.bind(self, T.class_of(Formula))
          url "foo-1.0"
          disable! date: disable_date_.to_s, because: :unsupported
        end
      end

      it "is deprecated before disable date" do
        allow(Date).to receive(:today).and_return(Date.parse(disable_date) << 12)
        expect(f.deprecated?).to be(true)
        expect(f.deprecation_reason).to be(:unsupported)
        expect(f.disabled?).to be(false)
        expect(f.disable_reason).to be_nil
      end

      it "is disabled on disable date" do
        allow(Date).to receive(:today).and_return(Date.parse(disable_date))
        expect(f.disabled?).to be(true)
        expect(f.disable_reason).to be(:unsupported)
      end
    end
  end

  describe ".all" do
    it "skips formulas that raise FormulaSpecificationError" do
      allow(described_class).to receive_messages(core_names: ["testball"], tap_files: [])
      allow(Formulary).to receive(:factory).with("testball").and_raise(
        FormulaSpecificationError, "testball: formula requires at least a URL"
      )

      expect { described_class.all(eval_all: true) }.not_to raise_error
      expect(described_class.all(eval_all: true)).to eq([])
    end

    it "skips untrusted tap formulae when trust is enabled" do
      tap = Tap.fetch("thirdparty", "foo")
      formula_path = tap.formula_dir/"untrusted.rb"
      formula_path.dirname.mkpath
      formula_path.write <<~RUBY
        raise "untrusted formula evaluated"
      RUBY

      allow(described_class).to receive_messages(core_names: [], tap_files: [formula_path])
      expect(Formulary).not_to receive(:factory).with(formula_path)

      with_env(HOMEBREW_REQUIRE_TAP_TRUST: "1") do
        expect { expect(described_class.all(eval_all: true)).to eq([]) }
          .to output(%r{Skipping thirdparty/foo because it is not trusted}).to_stderr
      end
    ensure
      FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
    end

    it "allows all formulae when trust is enabled" do
      allow(described_class).to receive_messages(core_names: [], tap_files: [])

      with_env(HOMEBREW_REQUIRE_TAP_TRUST: "1") do
        expect(described_class.all).to eq([])
      end
    end
  end

  describe "#std_cabal_v2_args" do
    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    it "allows changing the installation directory" do
      expect(f.std_cabal_v2_args(installdir: "/tmp/foo")).to include("--installdir=/tmp/foo")
    end

    it "excludes installation arguments when `installdir: false`" do
      expect(f.std_cabal_v2_args(installdir: false)).not_to include(a_string_starting_with("--install"))
    end

    context "when running on Linux", :needs_linux do
      it "includes flag for PIE on arm" do
        allow(Hardware::CPU).to receive(:arm?).and_return(true)
        expect(f.std_cabal_v2_args).to include("--ghc-option=-pie")
      end

      it "excludes flag for PIE on non-arm" do
        allow(Hardware::CPU).to receive(:arm?).and_return(false)
        expect(f.std_cabal_v2_args).not_to include("--ghc-option=-pie")
      end
    end
  end

  describe "#std_go_args" do
    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    it "defaults to stripping binaries" do
      expect(f.std_go_args).to include("-ldflags=-s -w")

      ldflags = "-X main.version=1.0.0"
      expect(f.std_go_args(ldflags:)).to include("-ldflags=-s -w #{ldflags}")
    end

    it "does not strip binaries when building with debug symbols" do
      allow(ENV).to receive(:debug_symbols?).and_return(true)
      expect(f.std_go_args).not_to include(a_string_starting_with("-ldflags"))

      ldflags = "-X main.version=1.0.0"
      expect(f.std_go_args(ldflags:)).to include("-ldflags=#{ldflags}")
    end

    it "raises an error when provided an invalid ldflags symbol" do
      expect { f.std_go_args(ldflags: :foo) }.to raise_error(ArgumentError, "Invalid ldflags: :foo")
    end

    context "with `ldflags: :goreleaser`" do
      subject(:std_go_args) { f.std_go_args(ldflags: :goreleaser) }

      let(:built_by) { "Homebrew" }
      let(:commit) { built_by }
      let(:date) { "2026-01-01T12:00:00Z" }
      let(:expected_ldflags) do
        "-s -w " \
          "-X 'main.version=1.0' " \
          "-X 'main.commit=#{commit}' " \
          "-X 'main.date=#{date}' " \
          "-X 'main.builtBy=#{built_by}'"
      end

      before { allow(f).to receive(:time).and_return(Time.parse(date)) }

      context "when in a git repository" do
        let(:buildpath) { mktmpdir }
        let(:commit) { Utils.popen_read("git", "-C", buildpath, "rev-parse", "HEAD").chomp }

        before { allow(f).to receive(:buildpath).and_return(buildpath) }

        around do |example|
          buildpath.cd do
            FileUtils.touch "LICENSE"
            system "git", "init"
            system "git", "add", "--all"
            system "git", "commit", "-m", "Initial commit"
            example.run
          end
        end

        it "uses git commit for main.commit" do
          expect(std_go_args).to include("-ldflags=#{expected_ldflags}")
        end
      end

      context "when not in a git repository and tap is available" do
        let(:built_by) { "someone" }

        before { allow(f).to receive(:tap).and_return(Tap.fetch(built_by, "repo")) }

        it "uses tap user for main.commit" do
          expect(std_go_args).to include("-ldflags=#{expected_ldflags}")
        end
      end

      context "when not in a git repository and tap is not available" do
        before { allow(f).to receive(:tap).and_return(nil) }

        it "uses Homebrew for main.commit" do
          expect(std_go_args).to include("-ldflags=#{expected_ldflags}")
        end
      end
    end

    it "includes a comma-separated list of input tags" do
      expect(f.std_go_args(tags: %w[foo bar baz])).to include("-tags=foo,bar,baz")
    end
  end

  describe "#std_pip_args" do
    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    it "filters packages uploaded within the last day" do
      expect(f.std_pip_args).to include("--uploaded-prior-to=P1D")
    end
  end

  describe "#std_swift_args" do
    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    it "allows controlling parallel jobs" do
      allow(ENV).to receive(:make_jobs).and_return(5)
      expect(f.std_swift_args.join(" ")).to include("--jobs 5")
    end

    it "disables non-writable sandbox path on macOS", :needs_macos do
      expect(f.std_swift_args).to include("--disable-sandbox")
    end

    it "includes override for ld shim on Linux", :needs_linux do
      expect(f.std_swift_args).to include("-use-ld=ld")
    end
  end

  describe "#std_zig_args" do
    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    it "raises an error when provided an unknown release mode" do
      expect { f.std_zig_args(release_mode: :test) }.to raise_error(ArgumentError)
    end

    it "includes equivalent Zig CPU for known target arch" do
      allow(ENV).to receive(:effective_arch).and_return(:arm_vortex_tempest)
      expect(f.std_zig_args).to include("-Dcpu=apple_m1")
    end

    it "allows overriding Zig CPU" do
      expect(f.std_zig_args(cpu: :generic)).to include("-Dcpu=generic")
    end
  end

  describe "#common_sandbox_env" do
    let(:f) do
      formula do
        T.bind(self, T.class_of(Formula))
        url "foo-1.0"
      end
    end

    it "sets Bundler cooldown for RubyGems dependencies" do
      expect(f.common_sandbox_env(mktmpdir)[:BUNDLE_COOLDOWN]).to eq("1")
    end

    it "prevents build tools from reading user configuration" do
      home = mktmpdir

      expect(f.common_sandbox_env(home)).to include(
        GIT_CONFIG_GLOBAL:     Utils::Git.no_global_config_file,
        GIT_TERMINAL_PROMPT:   "0",
        GOENV:                 "off",
        NPM_CONFIG_USERCONFIG: File::NULL,
        PIP_CONFIG_FILE:       File::NULL,
        XDG_CONFIG_HOME:       (home/".config").to_s,
      )
    end
  end

  describe ".no_autobump!" do
    it "raises an error when used in an unofficial tap" do
      unofficial_tap = Tap.fetch("someone", "repo")
      allow(Tap).to receive(:from_path).and_return(unofficial_tap)

      expect do
        Class.new(Formula) do
          no_autobump! because: "some reason"
        end
      end.to raise_error(ArgumentError, /official Homebrew taps/)
    end

    it "allows usage when tap is official" do
      official_tap = Tap.fetch("Homebrew", "core")
      allow(Tap).to receive(:from_path).and_return(official_tap)

      klass = Class.new(Formula) do
        no_autobump! because: "some reason"
      end
      expect(klass.autobump?).to be(false)
    end
  end
end
