# typed: false
# frozen_string_literal: true

RSpec.describe Cask::Cask, :cask do
  let(:cask) { described_class.new("versioned-cask") }

  def write_info_plist(path, short_version: nil, bundle_version: nil, contents: nil)
    info_plist = path/"Contents/Info.plist"
    info_plist.dirname.mkpath

    if contents
      info_plist.write(contents)
      return
    end

    entries = []
    if short_version
      entries << <<~PLIST.chomp
        <key>CFBundleShortVersionString</key>
        <string>#{short_version}</string>
      PLIST
    end
    if bundle_version
      entries << <<~PLIST.chomp
        <key>CFBundleVersion</key>
        <string>#{bundle_version}</string>
      PLIST
    end

    info_plist.write <<~PLIST
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
      <plist version="1.0">
      <dict>
      #{entries.join("\n")}
      </dict>
      </plist>
    PLIST
  end

  def write_auto_updates_cask(path, version:, artifacts:, token: "auto-updates-bundle-check")
    path.write <<~RUBY
      cask "#{token}" do
        version "#{version}"
        sha256 "5633c3a0f2e572cbf021507dec78c50998b398c343232bdfc7e26221d0a5db4d"

        url "file://#{TEST_FIXTURE_DIR}/cask/MyFancyApp.zip"
        homepage "https://brew.sh/MyFancyApp"

        auto_updates true

        #{artifacts.join("\n  ")}
      end
    RUBY

    Cask::CaskLoader.load(path)
  end

  describe ".all" do
    it "skips untrusted tap casks when trust is enabled" do
      tap = Tap.fetch("thirdparty", "foo")
      cask_path = tap.cask_dir/"untrusted.rb"
      cask_path.dirname.mkpath
      cask_path.write <<~RUBY
        raise "untrusted cask evaluated"
      RUBY

      allow(CoreCaskTap.instance).to receive(:cask_tokens).and_return([])
      allow(Tap).to receive(:reject).and_return([tap])
      expect(Cask::CaskLoader).not_to receive(:load).with(cask_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 casks when trust is disabled" do
      allow(CoreCaskTap.instance).to receive(:cask_tokens).and_return([])
      allow(Tap).to receive(:reject).and_return([])

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

    it "skips invalid casks instead of aborting" do
      tap = Tap.fetch("thirdparty", "foo")
      cask_path = tap.cask_dir/"mismatch.rb"
      cask_path.dirname.mkpath
      cask_path.write <<~RUBY
        cask "not-mismatch" do
          version "1.0"
          sha256 :no_check
          url "https://example.com/foo.zip"
          name "Foo"
          app "Foo.app"
        end
      RUBY

      allow(CoreCaskTap.instance).to receive(:cask_tokens).and_return([])
      allow(Tap).to receive(:reject).and_return([tap])

      with_env(HOMEBREW_NO_REQUIRE_TAP_TRUST: "1") do
        expect { expect(described_class.all).to eq([]) }
          .to output(/Cask 'mismatch' definition is invalid/).to_stderr
      end
    ensure
      FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
    end
  end

  describe "#any_version_installed?" do
    it "matches #installed?" do
      allow(cask).to receive(:installed?).and_return(true)

      expect(cask.any_version_installed?).to be true
    end
  end

  context "when multiple versions are installed" do
    describe "#installed_version" do
      context "when there are duplicate versions" do
        it "uses the last unique version" do
          Dir.mktmpdir do |dir|
            allow(Cask::Caskroom).to receive(:path).and_return(Pathname(dir))
            [
              ["1.2.2", "0999"],
              ["1.2.3", "1000"],
              ["1.2.2", "1001"],
            ].each do |version, timestamp|
              casks_dir = Pathname(dir)/"versioned-cask/.metadata/#{version}/#{timestamp}/Casks"
              casks_dir.mkpath
              # Installed caskfile must exist to count as installed.
              (casks_dir/"versioned-cask.rb").write("cask \"versioned-cask\"\n")
            end

            expect(cask.installed_version).to eq("1.2.2")
          end
        end
      end
    end
  end

  describe "#pinned?" do
    it "ignores and replaces a dangling pin" do
      HOMEBREW_PINNED_CASKS.mkpath
      cask.pin_path.make_relative_symlink(cask.caskroom_path/"missing")

      expect(cask).not_to be_pinned
      expect(cask.pinned_version).to be_nil

      allow(cask).to receive(:installed_version).and_return("1.0")
      (cask.caskroom_path/"1.0").mkpath
      cask.pin

      expect(cask).to be_pinned
      expect(cask.pinned_version).to eq("1.0")
    end

    it "replaces a regular file pin record" do
      HOMEBREW_PINNED_CASKS.mkpath
      cask.pin_path.write("not a symlink")
      allow(cask).to receive(:installed_version).and_return("1.0")
      (cask.caskroom_path/"1.0").mkpath

      cask.pin

      expect(cask).to be_pinned
      expect(cask.pin_path).to be_a_symlink
    end
  end

  describe "load" do
    let(:tap_path) { CoreCaskTap.instance.path }
    let(:file_dirname) { Pathname.new(__FILE__).dirname }
    let(:relative_tap_path) { tap_path.relative_path_from(file_dirname) }

    it "returns an instance of the Cask for the given token" do
      c = Cask::CaskLoader.load("local-caffeine")
      expect(c).to be_a(described_class)
      expect(c.token).to eq("local-caffeine")
    end

    it "returns an instance of the Cask from a specific file location" do
      c = Cask::CaskLoader.load("#{tap_path}/Casks/local-caffeine.rb")
      expect(c).to be_a(described_class)
      expect(c).not_to be_loaded_from_api
      expect(c).not_to be_loaded_from_internal_api
      expect(c.token).to eq("local-caffeine")
    end

    it "returns an instance of the Cask from a JSON file" do
      c = Cask::CaskLoader.load("#{TEST_FIXTURE_DIR}/cask/caffeine.json")
      expect(c).to be_a(described_class)
      expect(c).to be_loaded_from_api
      expect(c).not_to be_loaded_from_internal_api
      expect(c.token).to eq("caffeine")
    end

    it "returns an instance of the Cask from an internal JSON file" do
      c = Cask::CaskLoader.load("#{TEST_FIXTURE_DIR}/cask/caffeine.internal.json")
      expect(c).to be_a(described_class)
      expect(c).to be_loaded_from_api
      expect(c).to be_loaded_from_internal_api
      expect(c.token).to eq("caffeine")
    end

    it "returns an instance of the Cask from a URL", :needs_utils_curl do
      c = Cask::CaskLoader.load("file://#{tap_path}/Casks/local-caffeine.rb")
      expect(c).to be_a(described_class)
      expect(c.token).to eq("local-caffeine")
    end

    it "raises an error when failing to download a Cask from a URL", :needs_utils_curl do
      expect do
        Cask::CaskLoader.load("file://#{tap_path}/Casks/notacask.rb")
      end.to raise_error(Cask::CaskUnavailableError)
    end

    it "returns an instance of the Cask from a relative file location" do
      c = Cask::CaskLoader.load(relative_tap_path/"Casks/local-caffeine.rb")
      expect(c).to be_a(described_class)
      expect(c.token).to eq("local-caffeine")
    end

    it "uses exact match when loading by token" do
      expect(Cask::CaskLoader.load("test-opera").token).to eq("test-opera")
      expect(Cask::CaskLoader.load("test-opera-mail").token).to eq("test-opera-mail")
    end

    it "raises an error when attempting to load a Cask that doesn't exist" do
      expect do
        Cask::CaskLoader.load("notacask")
      end.to raise_error(Cask::CaskUnavailableError)
    end
  end

  describe "metadata" do
    it "proposes a versioned metadata directory name for each instance" do
      cask_token = "local-caffeine"
      c = Cask::CaskLoader.load(cask_token)
      metadata_timestamped_path = Cask::Caskroom.path.join(cask_token, ".metadata", c.version)
      expect(c.metadata_versioned_path.to_s).to eq(metadata_timestamped_path.to_s)
    end
  end

  describe "outdated" do
    it "ignores the Casks that have auto_updates true (without --greedy)" do
      c = Cask::CaskLoader.load("auto-updates")
      expect(c).not_to be_outdated
      expect(c.outdated_version).to be_nil
    end

    it "ignores the Casks that have version :latest (without --greedy)" do
      c = Cask::CaskLoader.load("version-latest-string")
      expect(c).not_to be_outdated
      expect(c.outdated_version).to be_nil
    end

    describe "versioned casks" do
      subject { cask.outdated_version }

      let(:cask) { described_class.new("basic-cask") }

      shared_examples "versioned casks" do |tap_version, expectations|
        test_each(expectations) do |(installed_version, expected_output)|
          context "when version #{installed_version.inspect} is installed and the tap version is #{tap_version}" do
            it {
              allow(cask).to receive_messages(installed_version:,
                                              version:           Cask::DSL::Version.new(tap_version))
              expect(cask).to receive(:outdated_version).and_call_original
              expect(subject).to eq expected_output
            }
          end
        end
      end

      describe "installed version is equal to tap version => not outdated" do
        include_examples "versioned casks", "1.2.3",
                         "1.2.3" => nil
      end

      describe "installed version is different than tap version => outdated" do
        include_examples "versioned casks", "1.2.4",
                         "1.2.3" => "1.2.3",
                         "1.2.4" => nil
      end
    end

    describe "auto-updating versioned casks with bundle metadata" do
      let(:dir) { Pathname(mktmpdir) }
      let(:cask_file) { dir/"auto-updates-bundle-check.rb" }
      let(:artifacts) { ['app "MyFancyApp.app"'] }

      before do
        allow(Homebrew::EnvConfig).to receive(:upgrade_auto_updates_casks?).and_return(true)
      end

      it "is outdated when the installed short version is lower than the tap version" do
        tap_version = "2.61"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.57", bundle_version: "2057")

        expect(cask.outdated_version).to eq("2.57")
      end

      it "is not outdated when auto-update upgrades are disabled" do
        allow(Homebrew::EnvConfig).to receive(:upgrade_auto_updates_casks?).and_return(false)
        tap_version = "2.61"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.57", bundle_version: "2057")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the short version matches and the bundle version is lower than a CSV candidate" do
        tap_version = "2.61,3000"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.61", bundle_version: "2057")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the short version matches and the bundle version matches any CSV candidate" do
        tap_version = "2.61,3000,2057"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.61", bundle_version: "2057")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the installed short version is higher than the tap version" do
        tap_version = "2.61"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.62", bundle_version: "2057")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the installed cask version already matches the tap version" do
        tap_version = "2.61"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.61")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.57", bundle_version: "2057")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the installed short version directly matches the tap version" do
        tap_version = "2.61"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.61", bundle_version: "2057")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the installed bundle version directly matches the tap version" do
        tap_version = "2057"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.61", bundle_version: "2057")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the installed short and bundle versions combine to the tap version" do
        tap_version = "2.61-2057"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57-2056")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.61", bundle_version: "2057")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the combined installed version is higher than the tap version" do
        tap_version = "2.61-2057"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57-2056")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.61", bundle_version: "2058")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the installed short version matches a CSV build candidate" do
        tap_version = "2.61,2057"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2057", bundle_version: "3000")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the short version matches and the bundle version is higher than all CSV candidates" do
        tap_version = "2.61,2056,2055"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.61", bundle_version: "2057")

        expect(cask.outdated_version).to be_nil
      end

      it "matches a bundle version candidate that is not first in the CSV list" do
        tap_version = "2.61,3000,2057"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "2.61", bundle_version: "2057")

        expect(cask.version.csv.first).not_to eq("2057")
        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when the app bundle metadata cannot be read" do
        tap_version = "2.61"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")

        expect(cask.outdated_version).to be_nil
      end

      it "falls back to the bundle version when the short version is missing" do
        tap_version = "2.61,3000"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2.57")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", bundle_version: "2057")

        expect(cask.outdated_version).to eq("2.57")
      end

      it "ignores bad bundle versions when the short version is missing" do
        tap_version = "2026.406.0"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("2025.816.0")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", bundle_version: "0.0")

        expect(cask.outdated_version).to be_nil
      end

      it "is not outdated when plist version segment counts differ from the tap version" do
        tap_version = "1.0"
        cask = write_auto_updates_cask(cask_file, version: tap_version, artifacts:)
        allow(cask).to receive(:installed_version).and_return("0.9")
        write_info_plist(cask.config.appdir/"MyFancyApp.app", short_version: "1", bundle_version: "200")

        expect(cask.outdated_version).to be_nil
      end
    end

    describe ":latest casks" do
      let(:cask) { described_class.new("basic-cask") }

      shared_examples ":latest cask" do |greedy, outdated_sha, tap_version, expectations|
        test_each(expectations) do |(installed_version, expected_output)|
          context "when versions #{installed_version} are installed and the " \
                  "tap version is #{tap_version}, #{"not " unless greedy}greedy " \
                  "and sha is #{"not " unless outdated_sha}outdated" do
            subject { cask.outdated_version(greedy:) }

            it {
              allow(cask).to receive_messages(installed_version:,
                                              version:                Cask::DSL::Version.new(tap_version),
                                              outdated_download_sha?: outdated_sha)
              expect(cask).to receive(:outdated_version).and_call_original
              expect(subject).to eq expected_output
            }
          end
        end
      end

      describe ":latest version installed, :latest version in tap" do
        include_examples ":latest cask", false, false, "latest",
                         "latest" => nil
        include_examples ":latest cask", true, false, "latest",
                         "latest" => nil
        include_examples ":latest cask", true, true, "latest",
                         "latest" => "latest"
      end

      describe "numbered version installed, :latest version in tap" do
        include_examples ":latest cask", false, false, "latest",
                         "1.2.3" => nil
        include_examples ":latest cask", true, false, "latest",
                         "1.2.3" => nil
        include_examples ":latest cask", true, true, "latest",
                         "1.2.3" => "1.2.3"
      end

      describe "latest version installed, numbered version in tap" do
        include_examples ":latest cask", false, false, "1.2.3",
                         "latest" => "latest"
        include_examples ":latest cask", true, false, "1.2.3",
                         "latest" => "latest"
        include_examples ":latest cask", true, true, "1.2.3",
                         "latest" => "latest"
      end
    end
  end

  describe "full_name" do
    context "when it is a core cask" do
      it "is the cask token" do
        c = Cask::CaskLoader.load("local-caffeine")
        expect(c.full_name).to eq("local-caffeine")
      end
    end

    context "when it is from a non-core tap" do
      it "returns the fully-qualified name of the cask" do
        c = with_env(HOMEBREW_NO_REQUIRE_TAP_TRUST: "1") do
          Cask::CaskLoader.load("third-party/tap/third-party-cask")
        end
        expect(c.full_name).to eq("third-party/tap/third-party-cask")
      end
    end

    context "when it is from no known tap" do
      it "returns the cask token" do
        file = Tempfile.new(%w[tapless-cask .rb])

        begin
          cask_name = File.basename(file.path, ".rb")
          file.write "cask '#{cask_name}'"
          file.close

          c = Cask::CaskLoader.load(file.path)
          expect(c.full_name).to eq(cask_name)
        ensure
          file.close
          file.unlink
        end
      end
    end
  end

  describe "#artifacts_list" do
    subject(:cask) { Cask::CaskLoader.load("many-artifacts") }

    it "returns all artifacts when no options are given" do
      expected_artifacts = [
        { uninstall_preflight: nil },
        { preflight: nil },
        { uninstall: [{
          rmdir: "#{TEST_TMPDIR}/empty_directory_path",
          trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
        }] },
        { pkg: ["ManyArtifacts/ManyArtifacts.pkg"] },
        { app: ["ManyArtifacts/ManyArtifacts.app"], target: "#{TEST_TMPDIR}/cask-appdir/ManyArtifacts.app" },
        { uninstall_postflight: nil },
        { postflight: nil },
        { zap: [{
          rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
          trash: "~/Library/Logs/ManyArtifacts.log",
        }] },
      ]

      expect(cask.artifacts_list).to eq(expected_artifacts)
    end

    it "returns only uninstall artifacts when uninstall_only is true" do
      expected_artifacts = [
        { uninstall_preflight: nil },
        { uninstall: [{
          rmdir: "#{TEST_TMPDIR}/empty_directory_path",
          trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
        }] },
        { app: ["ManyArtifacts/ManyArtifacts.app"] },
        { uninstall_postflight: nil },
        { zap: [{
          rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
          trash: "~/Library/Logs/ManyArtifacts.log",
        }] },
      ]

      expect(cask.artifacts_list(uninstall_only: true)).to eq(expected_artifacts)
    end
  end

  describe "#rename_list" do
    subject(:cask) { Cask::CaskLoader.load("many-renames") }

    it "returns the correct rename list" do
      expect(cask.rename_list).to eq([
        { from: "Foobar.app", to: "Foo.app" },
        { from: "Foo.app", to: "Bar.app" },
      ])
    end
  end

  describe "#uninstall_flight_blocks?" do
    matcher :have_uninstall_flight_blocks do
      match do |actual|
        actual.uninstall_flight_blocks? == true
      end
    end

    it "returns true when there are uninstall_preflight blocks" do
      cask = Cask::CaskLoader.load("with-uninstall-preflight")
      expect(cask).to have_uninstall_flight_blocks
    end

    it "returns true when there are uninstall_postflight blocks" do
      cask = Cask::CaskLoader.load("with-uninstall-postflight")
      expect(cask).to have_uninstall_flight_blocks
    end

    it "returns false when there are only preflight blocks" do
      cask = Cask::CaskLoader.load("with-preflight")
      expect(cask).not_to have_uninstall_flight_blocks
    end

    it "returns false when there are only postflight blocks" do
      cask = Cask::CaskLoader.load("with-postflight")
      expect(cask).not_to have_uninstall_flight_blocks
    end

    it "returns false when there are no flight blocks" do
      cask = Cask::CaskLoader.load("local-caffeine")
      expect(cask).not_to have_uninstall_flight_blocks
    end
  end

  describe "#supports_linux?" do
    it "uses explicit OS dependencies and defaults to Linux support" do
      expect(Cask::CaskLoader.load("with-depends-on-macos-bare").supports_linux?).to be false
      expect(Cask::CaskLoader.load("with-depends-on-maximum-macos").supports_linux?).to be false
      expect(Cask::CaskLoader.load("with-depends-on-macos-in-on-macos").supports_linux?).to be true
      expect(Cask::CaskLoader.load("with-depends-on-linux-bare").supports_linux?).to be true

      expect(Cask::CaskLoader.load("with-non-executable-binary").supports_linux?).to be true
      expect(Cask::CaskLoader.load("basic-cask").supports_linux?).to be true
      expect(Cask::CaskLoader.load("with-installer-manual").supports_linux?).to be true
    end
  end

  describe "#supports_macos?" do
    it "returns false for casks with bare depends_on :linux" do
      expect(Cask::CaskLoader.load("with-depends-on-linux-bare").supports_macos?).to be false
    end
  end

  describe "#artifacts_supported_on_os?" do
    it "rejects unknown operating systems" do
      expect { cask.artifacts_supported_on_os?(:windows) }
        .to raise_error(ArgumentError, "Unsupported operating system: :windows")
    end
  end

  describe "#outdated_info" do
    it "includes pinned cask details" do
      cask = Cask::CaskLoader.load("local-caffeine")
      allow(cask).to receive_messages(outdated_version: "1.2.2", pinned?: true, pinned_version: "1.2.2")

      expect(cask.outdated_info(false, true, false, false, false))
        .to eq("local-caffeine (1.2.2) != 1.2.3 [pinned at 1.2.2]")
      expect(cask.outdated_info(false, false, true, false, false)).to include(
        pinned:         true,
        pinned_version: "1.2.2",
      )
    end
  end

  describe "#to_h" do
    let(:expected_json) do
      (TEST_FIXTURE_DIR/"cask/everything.json").read.strip.gsub("$APPDIR", "#{TEST_TMPDIR}/cask-appdir")
    end

    context "when loaded from cask file" do
      it "returns expected hash" do
        allow(MacOS).to receive(:version).and_return(MacOSVersion.new("14"))

        cask = Cask::CaskLoader.load("everything")

        expect(cask.tap).to receive(:git_head).and_return("abcdef1234567890abcdef1234567890abcdef12")

        hash = cask.to_h

        expect(hash).to be_a(Hash)
        expect(JSON.pretty_generate(hash)).to eq(expected_json)
      end
    end

    context "when loaded from json file" do
      it "returns expected hash" do
        expect(Homebrew::API::Cask).not_to receive(:source_download)
        hash = Cask::CaskLoader::FromAPILoader.new(
          "everything", from_json: JSON.parse(expected_json)
        ).load(config: nil).to_h

        expect(hash).to be_a(Hash)
        expect(JSON.pretty_generate(hash)).to eq(expected_json)
      end
    end
  end

  describe "#refresh_for_tag" do
    let(:cask) { Cask::CaskLoader.load("on-linux-asymmetric") }

    it "yields with the cask refreshed for a supported tag" do
      tag = Utils::Bottles::Tag.new(system: :sonoma, arch: :intel)
      expect(cask.refresh_for_tag(tag) { cask.url.to_s }).to include("caffeine-intel-darwin")
    end

    it "yields for a Linux architecture whose checksum is missing" do
      tag = Utils::Bottles::Tag.new(system: :linux, arch: :arm)
      expect(cask.refresh_for_tag(tag) { cask.url.to_s }).to include("caffeine-arm-linux")
    end

    it "returns nil for a tag the cask cannot be refreshed for" do
      invalid_on_linux_cask = described_class.new("on-linux-invalid") do
        on_macos do
          version "1.2.3"
        end
        sha256 :no_check
        url "https://brew.sh/foo-#{version.major_minor}.zip"
      end

      tag = Utils::Bottles::Tag.new(system: :linux, arch: :arm)
      expect(invalid_on_linux_cask.refresh_for_tag(tag) { invalid_on_linux_cask.url }).to be_nil
    end
  end

  describe "#to_hash_with_variations" do
    let!(:original_macos_version) { MacOS.full_version.to_s }
    let(:expected_versions_variations) do
      <<~JSON
        {
          "golden_gate": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.2.3/intel.zip"
          },
          "tahoe": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.2.3/intel.zip"
          },
          "sequoia": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.2.3/intel.zip"
          },
          "sonoma": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.2.3/intel.zip"
          },
          "ventura": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.2.3/intel.zip"
          },
          "monterey": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.2.3/intel.zip"
          },
          "big_sur": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.2.0/intel.zip",
            "version": "1.2.0",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "arm64_big_sur": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin-arm64/1.2.0/arm.zip",
            "version": "1.2.0",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "catalina": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.0.0/intel.zip",
            "version": "1.0.0",
            "sha256": "1866dfa833b123bb8fe7fa7185ebf24d28d300d0643d75798bc23730af734216"
          },
          "x86_64_linux": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin//intel.zip",
            "version": null,
            "sha256": null
          },
          "arm64_linux": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin-arm64//arm.zip",
            "version": null,
            "sha256": null
          }
        }
      JSON
    end
    let(:expected_sha256_variations) do
      <<~JSON
        {
          "golden_gate": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "tahoe": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "sequoia": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "sonoma": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "ventura": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "monterey": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "big_sur": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "catalina": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "x86_64_linux": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
            "sha256": null
          },
          "arm64_linux": {
            "sha256": null
          }
        }
      JSON
    end
    let(:expected_sha256_variations_os) do
      <<~JSON
        {
          "golden_gate": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel-darwin.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "tahoe": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel-darwin.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "sequoia": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel-darwin.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "sonoma": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel-darwin.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "ventura": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel-darwin.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "monterey": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel-darwin.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "big_sur": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel-darwin.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "catalina": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel-darwin.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "x86_64_linux": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel-linux.zip",
            "sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
          },
          "arm64_linux": {
            "url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-arm-linux.zip"
          }
        }
      JSON
    end

    before do
      # For consistency, always run on Monterey and ARM
      MacOS.full_version = "12"
      allow(Hardware::CPU).to receive(:type).and_return(:arm)
    end

    after do
      MacOS.full_version = original_macos_version
    end

    it "returns language variations with a deterministic default" do
      hash = JSON.parse(JSON.generate(Cask::CaskLoader.load("with-languages").to_hash_with_variations))

      expect(hash.slice("url", "sha256", "language_variations")).to eq({
        "url"                 => "file://#{TEST_FIXTURE_DIR}/cask/caffeine.zip",
        "sha256"              => "67cdb8a02803ef37fdbf7e0be205863172e41a561ca446cd84f0d7ab35a99d94",
        "language_variations" => [
          {
            "languages" => ["zh"],
            "default"   => false,
            "value"     => "zh-CN",
            "url"       => "file://#{TEST_FIXTURE_DIR}/cask/container.tar.gz",
            "sha256"    => "fab685fabf73d5a9382581ce8698fce9408f5feaa49fa10d9bc6c510493300f5",
            "artifacts" => [{
              "app"    => ["Container.app"],
              "target" => "#{TEST_TMPDIR}/cask-appdir/Container.app",
            }],
          },
          { "languages" => ["en-US"], "default" => true, "value" => "en-US" },
        ],
      })
    end

    it "preserves the cask configuration while generating language variations" do
      appdir = Pathname(TEST_TMPDIR)/"configured-appdir"
      config = Cask::Config.from_json({ default: { appdir:, languages: ["en-US"] } }.to_json)
      hash = Cask::CaskLoader.load("with-languages", config:).to_hash_with_variations

      expect([
        hash.dig("artifacts", 0, :target),
        hash.dig("language_variations", 0, "artifacts", 0, :target),
      ]).to eq([appdir/"Caffeine.app", appdir/"Container.app"].map(&:to_s))
    end

    it "returns the correct variations hash for a cask with multiple versions" do
      c = Cask::CaskLoader.load("multiple-versions")
      h = c.to_hash_with_variations

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

    it "returns the correct variations hash for a cask different sha256s on each arch" do
      c = Cask::CaskLoader.load("sha256-arch")
      h = c.to_hash_with_variations

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

    it "returns the correct variations hash for a cask different sha256s on each arch and os" do
      c = Cask::CaskLoader.load("sha256-os")
      h = c.to_hash_with_variations

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

    it "emits variations without checksums for Linux architectures a cask omits" do
      c = Cask::CaskLoader.load("on-linux-asymmetric")
      h = JSON.parse(JSON.generate(c.to_hash_with_variations))

      expect(h["variations"]["arm64_linux"]).to include(
        "depends_on" => { "arch" => [{ "type" => "intel", "bits" => 64 }] },
        "sha256"     => nil,
      )
    end

    it "emits Linux variations for a cask with Linux checksums but no `os` stanza" do
      c = Cask::CaskLoader.load("sha256-linux")
      h = c.to_hash_with_variations

      expect(h["variations"]).to include(:x86_64_linux, :arm64_linux)
    end

    it "emits Linux variations with checksums for a Linux-only cask" do
      c = Cask::CaskLoader.load("sha256-linux-only")
      h = c.to_hash_with_variations

      expect(h["variations"].slice(:x86_64_linux, :arm64_linux).transform_values { |v| v["sha256"].to_s }).to eq(
        x86_64_linux: "244d413861cecb3707cfbcc5c4346d5367daa827da5ea08fb3f3bc2b6276d239",
        arm64_linux:  "9a1c0967baa46828930ccbbc88668d1b0db07e6edf778800ed4da073c00054f8",
      )
    end

    it "emits Linux variations for a cask with `on_linux` content but no `os` stanza" do
      c = Cask::CaskLoader.load("on-linux-blocks")
      h = JSON.parse(JSON.generate(c.to_hash_with_variations))

      app_image_artifacts = [{
        "app_image" => ["Caffeine.AppImage"],
        "target"    => "#{TEST_TMPDIR}/cask-appimagedir/Caffeine.AppImage",
      }]
      expect(h["variations"].slice("x86_64_linux", "arm64_linux").transform_values do |v|
        v.slice("sha256", "artifacts")
      end).to eq(
        "x86_64_linux" => {
          "sha256"    => "244d413861cecb3707cfbcc5c4346d5367daa827da5ea08fb3f3bc2b6276d239",
          "artifacts" => app_image_artifacts,
        },
        "arm64_linux"  => {
          "sha256"    => "9a1c0967baa46828930ccbbc88668d1b0db07e6edf778800ed4da073c00054f8",
          "artifacts" => app_image_artifacts,
        },
      )
    end

    context "when recording supported platforms" do
      let(:platform_tags) do
        [
          Utils::Bottles::Tag.new(system: :sonoma, arch: :intel),
          Utils::Bottles::Tag.new(system: :sonoma, arch: :arm),
          Utils::Bottles::Tag.new(system: :monterey, arch: :intel),
          Utils::Bottles::Tag.new(system: :monterey, arch: :arm),
          Utils::Bottles::Tag.new(system: :catalina, arch: :intel),
          Utils::Bottles::Tag.new(system: :linux, arch: :intel),
          Utils::Bottles::Tag.new(system: :linux, arch: :arm),
        ]
      end
      let(:macos_platforms) { [:sonoma, :arm64_sonoma, :monterey, :arm64_monterey, :catalina] }

      before do
        stub_const("OnSystem::VALID_OS_ARCH_TAGS", platform_tags)
      end

      it "records platforms allowed by scoped macOS requirements" do
        c = Cask::CaskLoader.load("with-depends-on-macos-in-on-macos")

        expect(c.to_hash_with_variations["supported_platforms"]).to eq(
          [:sonoma, :arm64_sonoma, :monterey, :arm64_monterey, :x86_64_linux, :arm64_linux],
        )
      end

      it "excludes platforms without complete download data" do
        c = Cask::CaskLoader.load("multiple-versions")

        expect(c.to_hash_with_variations["supported_platforms"]).to eq(macos_platforms)
      end

      it "excludes platforms rejected by architecture requirements" do
        c = described_class.new("architecture-restricted") do
          version :latest
          arch arm: "arm64", intel: "x86_64"
          sha256 :no_check
          url "https://brew.sh/#{arch}.zip"
          on_linux do
            depends_on arch: :x86_64
          end
          binary "foo"
        end

        expect(c.to_hash_with_variations["supported_platforms"]).to eq(
          [*macos_platforms, :x86_64_linux],
        )
      end

      it "excludes Linux for macOS-only artifacts" do
        c = described_class.new("macos-artifact") do
          version :latest
          arch arm: "arm64", intel: "x86_64"
          sha256 :no_check
          url "https://brew.sh/#{arch}.zip"
          app "Foo.app"
        end

        expect(c.to_hash_with_variations["supported_platforms"]).to eq(macos_platforms)
      end

      it "excludes Linux for manual installers" do
        c = described_class.new("manual-installer") do
          version :latest
          arch arm: "arm64", intel: "x86_64"
          sha256 :no_check
          url "https://brew.sh/#{arch}.zip"
          installer manual: "Foo.app"
        end

        expect(c.to_hash_with_variations["supported_platforms"]).to eq(macos_platforms)
      end

      it "excludes macOS for Linux-only artifacts" do
        c = described_class.new("linux-artifact") do
          version :latest
          arch arm: "arm64", intel: "x86_64"
          sha256 :no_check
          url "https://brew.sh/#{arch}.zip"
          app_image "Foo.AppImage"
        end

        expect(c.to_hash_with_variations["supported_platforms"]).to eq([:x86_64_linux, :arm64_linux])
      end

      it "includes stage-only casks" do
        c = described_class.new("stage-only") do
          version :latest
          arch arm: "arm64", intel: "x86_64"
          sha256 :no_check
          url "https://brew.sh/#{arch}.zip"
          stage_only true
        end

        expect(c.to_hash_with_variations["supported_platforms"]).to eq(platform_tags.map(&:to_sym))
      end

      it "records no supported platforms for a cask without an installable artifact" do
        c = described_class.new("zap-only") do
          version :latest
          arch arm: "arm64", intel: "x86_64"
          sha256 :no_check
          url "https://brew.sh/#{arch}.zip"
          zap trash: "~/Library/Caches/brew-test"
        end

        expect(c.to_hash_with_variations["supported_platforms"]).to eq([])
      end

      it "records every platform when a cask has no platform variations" do
        c = described_class.new("no-platform-variations") do
          version :latest
          sha256 :no_check
          url "https://brew.sh/foo.zip"
          binary "foo"
        end

        expect(c.to_hash_with_variations["supported_platforms"]).to eq(platform_tags.map(&:to_sym))
      end

      it "records top-level platform requirements without variations" do
        c = described_class.new("top-level-platform-requirements") do
          version :latest
          sha256 :no_check
          url "https://brew.sh/foo.zip"
          depends_on macos: :monterey
          depends_on arch: :x86_64
          binary "foo"
        end

        expect(c.to_hash_with_variations["supported_platforms"]).to eq([:sonoma, :monterey])
      end

      it "serializes architecture-varying and universal casks with the same macOS requirement" do
        tags = OnSystem::ALL_OS_ARCH_COMBINATIONS.filter_map do |os, arch|
          tag = Utils::Bottles::Tag.new(system: os, arch:)
          tag if tag.valid_combination?
        end
        stub_const("OnSystem::VALID_OS_ARCH_TAGS", tags)

        architecture_varying = described_class.new("architecture-varying") do
          arch arm: "ARM64", intel: "64"
          version "1.2.3"
          sha256 arm:   "a" * 64,
                 intel: "b" * 64
          url "https://brew.sh/#{arch}.zip"
          depends_on macos: :big_sur
          app "Foo.app"
        end
        universal = described_class.new("universal") do
          version :latest
          sha256 :no_check
          url "https://brew.sh/foo.zip"
          depends_on macos: :big_sur
          app "Foo.app"
        end

        architecture_varying.to_hash_with_variations
        supported_platforms = Timeout.timeout(5) do
          universal.to_hash_with_variations["supported_platforms"]
        end

        expected_platforms = tags.filter_map do |tag|
          tag.to_sym if tag.macos? && tag.system != :catalina
        end
        expect(supported_platforms).to eq(expected_platforms)
      end

      it "isolates macOS requirement comparisons between casks" do
        supported_platforms = [:monterey, :sonoma].map do |minimum_macos|
          c = described_class.new("requires-#{minimum_macos}") do
            version :latest
            sha256 :no_check
            url "https://brew.sh/foo.zip"
            depends_on macos: minimum_macos
            binary "foo"
          end

          c.to_hash_with_variations["supported_platforms"]
        end

        expect(supported_platforms).to eq(
          [
            [:sonoma, :arm64_sonoma, :monterey, :arm64_monterey],
            [:sonoma, :arm64_sonoma],
          ],
        )
      end
    end

    # NOTE: The calls to `Cask.generating_hash!` and `Cask.generated_hash!`
    #       are not idempotent so they can only be used in one test.
    it "returns the correct hash placeholders" do
      described_class.generating_hash!
      expect(described_class).to be_generating_hash
      c = Cask::CaskLoader.load("placeholders")
      h = c.to_hash_with_variations
      described_class.generated_hash!
      expect(described_class).not_to be_generating_hash

      expect(h).to be_a(Hash)
      expect(h["artifacts"].first[:binary].first).to eq "$APPDIR/some/path"
      expect(h["caveats"]).to eq "$HOMEBREW_PREFIX and /$HOME\n"
    end

    context "when loaded from json file" do
      let(:expected_json) do
        (TEST_FIXTURE_DIR/"cask/everything-with-variations.json").read.strip
          .gsub("$APPDIR", "#{TEST_TMPDIR}/cask-appdir")
      end

      it "returns expected hash with variations" do
        expect(Homebrew::API::Cask).not_to receive(:source_download)
        cask = Cask::CaskLoader::FromAPILoader.new("everything-with-variations", from_json: JSON.parse(expected_json))
                                              .load(config: nil)

        hash = cask.to_hash_with_variations

        expect(cask.loaded_from_api?).to be true
        expect(cask.loaded_from_internal_api?).to be false
        expect(hash).to be_a(Hash)
        expect(JSON.pretty_generate(hash)).to eq(expected_json)
      end
    end

    it "does not include macOS dependency in Linux variations" do
      c = Cask::CaskLoader.load("sha256-os")
      h = c.to_hash_with_variations

      [:x86_64_linux, :arm64_linux].each do |tag|
        merged = Homebrew::API.merge_variations(h.deep_dup, bottle_tag: Utils::Bottles::Tag.from_symbol(tag))
        expect(merged["depends_on"]).not_to include("macos") if merged["depends_on"].present?
      end
    end
  end
end
