# typed: false
# frozen_string_literal: true

require "extend/ENV"
require "requirement"

RSpec.describe Requirement do
  alias_matcher :be_a_build_requirement, :be_a_build

  subject(:requirement) { klass.new }

  let(:klass) { Class.new(described_class) }

  describe "base class" do
    it "raises an error when instantiated" do
      expect { described_class.new }
        .to raise_error(RuntimeError, "Requirement is declared as abstract; it cannot be instantiated")
    end
  end

  describe "#tags" do
    subject(:req) { klass.new(tags) }

    context "with a single tag" do
      let(:tags) { ["bar"] }

      it(:tags) { expect(req.tags).to eq(tags) }
    end

    context "with multiple tags" do
      let(:tags) { ["bar", "baz"] }

      it(:tags) { expect(req.tags).to eq(tags) }
    end

    context "with symbol tags" do
      let(:tags) { [:build] }

      it(:tags) { expect(req.tags).to eq(tags) }
    end

    context "with symbol and string tags" do
      let(:tags) { [:build, "bar"] }

      it(:tags) { expect(req.tags).to eq(tags) }
    end
  end

  describe "#fatal?" do
    describe "#fatal true is specified" do
      let(:klass) do
        Class.new(described_class) do
          fatal true
        end
      end

      it { is_expected.to be_fatal }
    end

    describe "#fatal is omitted" do
      it { is_expected.not_to be_fatal }
    end
  end

  describe "#satisfied?" do
    describe "#satisfy with block and build_env returns true" do
      let(:klass) do
        Class.new(described_class) do
          satisfy(build_env: false) do
            true
          end
        end
      end

      it { is_expected.to be_satisfied }
    end

    describe "#satisfy with block and build_env returns false" do
      let(:klass) do
        Class.new(described_class) do
          satisfy(build_env: false) do
            false
          end
        end
      end

      it { is_expected.not_to be_satisfied }
    end

    describe "#satisfy returns true" do
      let(:klass) do
        Class.new(described_class) do
          satisfy true
        end
      end

      it { is_expected.to be_satisfied }
    end

    describe "#satisfy returns false" do
      let(:klass) do
        Class.new(described_class) do
          satisfy false
        end
      end

      it { is_expected.not_to be_satisfied }
    end

    describe "#satisfy with block returning true and without :build_env" do
      let(:klass) do
        Class.new(described_class) do
          satisfy do
            true
          end
        end
      end

      it "sets up build environment" do
        expect(ENV).to receive(:with_build_environment).and_call_original
        requirement.satisfied?
      end
    end

    describe "#satisfy with block returning true and :build_env set to false" do
      let(:klass) do
        Class.new(described_class) do
          satisfy(build_env: false) do
            true
          end
        end
      end

      it "skips setting up build environment" do
        expect(ENV).not_to receive(:with_build_environment)
        requirement.satisfied?
      end
    end

    describe "#satisfy with block returning path and without :build_env" do
      let(:klass) do
        Class.new(described_class) do
          satisfy do
            Pathname.new("/foo/bar/baz")
          end
        end
      end

      it "infers path from #satisfy result" do
        without_partial_double_verification do
          expect(ENV).to receive(:prepend_path).with("PATH", Pathname.new("/foo/bar"))
        end
        requirement.satisfied?
        requirement.modify_build_environment
      end
    end

    describe "#satisfy with block returning path under HOMEBREW_PREFIX/bin" do
      let(:klass) do
        Class.new(described_class) do
          satisfy do
            HOMEBREW_PREFIX/"bin/foo"
          end
        end
      end

      it "does not prepend the parent to PATH" do
        without_partial_double_verification do
          expect(ENV).not_to receive(:prepend_path)
        end
        requirement.satisfied?
        requirement.modify_build_environment
      end
    end

    describe "#satisfy with block returning path under HOMEBREW_PREFIX/sbin" do
      let(:klass) do
        Class.new(described_class) do
          satisfy do
            HOMEBREW_PREFIX/"sbin/foo"
          end
        end
      end

      it "does not prepend the parent to PATH" do
        without_partial_double_verification do
          expect(ENV).not_to receive(:prepend_path)
        end
        requirement.satisfied?
        requirement.modify_build_environment
      end
    end

    describe "#satisfy with block calling #which and :build_env set to false" do
      let(:klass) do
        Class.new(described_class) do
          satisfy(build_env: false) do
            which("sh")
          end
        end
      end

      it "does not raise an error" do
        expect { requirement.satisfied? }.not_to raise_error
      end
    end
  end

  describe "#build?" do
    context "when the :build tag is specified" do
      subject { klass.new([:build]) }

      it { is_expected.to be_a_build_requirement }
    end

    describe "#build omitted" do
      it { is_expected.not_to be_a_build_requirement }
    end
  end

  describe "#name and #option_names" do
    let(:const) { :FooRequirement }
    # The stubbed requirement class is resolved by name at runtime.
    # rubocop:disable Sorbet/ConstantsFromStrings
    let(:klass) { self.class.const_get(const) }
    # rubocop:enable Sorbet/ConstantsFromStrings

    before do
      stub_const const.to_s, Class.new(described_class)
    end

    it(:name) { expect(requirement.name).to eq("foo") }
    it(:option_names) { expect(requirement.option_names).to eq(["foo"]) }
  end

  describe "#modify_build_environment" do
    context "without env proc" do
      let(:klass) { Class.new(described_class) }

      it "returns nil" do
        expect { requirement.modify_build_environment }.not_to raise_error
      end
    end
  end

  describe "#eql? and #==" do
    subject(:requirement) { klass.new }

    it "returns true if the names and tags are equal" do
      other = klass.new

      expect(requirement).to eql(other)
      expect(requirement).to eq(other)
    end

    it "returns false if names differ" do
      other = klass.new
      allow(other).to receive(:name).and_return("foo")
      expect(requirement).not_to eql(other)
      expect(requirement).not_to eq(other)
    end

    it "returns false if tags differ" do
      other = klass.new([:optional])

      expect(requirement).not_to eql(other)
      expect(requirement).not_to eq(other)
    end
  end

  describe "#hash" do
    subject(:requirement) { klass.new }

    it "is equal if names and tags are equal" do
      other = klass.new
      expect(requirement.hash).to eq(other.hash)
    end

    it "differs if names differ" do
      other = klass.new
      allow(other).to receive(:name).and_return("foo")
      expect(requirement.hash).not_to eq(other.hash)
    end

    it "differs if tags differ" do
      other = klass.new([:optional])
      expect(requirement.hash).not_to eq(other.hash)
    end
  end
end
