# typed: true
# frozen_string_literal: true

require "bundle"
require "bundle/dsl"
require "bundle/extensions/mac_app_store"

RSpec.describe Homebrew::Bundle::MacAppStore do
  describe "dumping" do
    subject(:dumper) { described_class }

    context "when mas is not installed" do
      before do
        described_class.reset!
        allow(described_class).to receive(:package_manager_executable).and_return(nil)
      end

      specify do
        expect(dumper.apps).to be_empty
        expect(dumper.dump).to eql("")
      end
    end

    context "when there is no apps" do
      before do
        described_class.reset!
        allow(described_class).to receive_messages(package_manager_executable: Pathname.new("mas"),
                                                   "`":                        "")
      end

      specify do
        expect(dumper.apps).to be_empty
        expect(dumper.dump).to eql("")
      end
    end

    context "when apps `foo`, `bar` and `baz` are installed" do
      before do
        described_class.reset!
        allow(described_class).to receive_messages(
          package_manager_executable: Pathname.new("mas"),
          "`":                        "123 foo (1.0)\n" \
                                      "456 bar (2.0)\n" \
                                      "789 baz (3.0)",
        )
      end

      it "returns list %w[foo bar baz]" do
        expect(dumper.apps).to eql([["123", "foo"], ["456", "bar"], ["789", "baz"]])
      end
    end

    context "when apps `foo`, `bar`, `baz` and `qux` are installed including right-justified IDs" do
      before do
        described_class.reset!
        allow(described_class).to receive(:package_manager_executable).and_return(Pathname.new("mas"))
        allow(described_class).to receive(:`).and_return("123 foo (1.0)\n456 bar (2.0)\n789 baz (3.0)")
        allow(described_class).to receive(:`).and_return("123 foo (1.0)\n456 bar (2.0)\n789 baz (3.0)\n 10 qux (4.0)")
      end

      it "returns list %w[foo bar baz qux]" do
        expect(dumper.apps).to eql([["123", "foo"], ["456", "bar"], ["789", "baz"], ["10", "qux"]])
      end
    end

    context "with invalid app details" do
      let(:invalid_mas_output) do
        <<~HEREDOC
          497799835 Xcode (9.2)
          425424353 The Unarchiver (4.0.0)
          08981434 iMovie (10.1.8)
          409201541 Pages (7.1)
          123456789 123AppNameWithNumbers (1.0)
          409203825 Numbers (5.1)
          944924917 Pastebin It! (1.0)
          123456789 My (cool) app (1.0)
          987654321 an-app-i-use (2.1)
          123457867 App name with many spaces (1.0)
          893489734 my,comma,app (2.2)
          832423434 another_app_name (1.0)
          543213432 My App? (1.0)
          688963445 app;with;semicolons (1.0)
          123345384 my 😊 app (2.0)
          896732467 你好 (1.1)
          634324555 مرحبا (1.0)
          234324325 áéíóú (1.0)
          310633997 non>‎<printing>⁣<characters (1.0)
        HEREDOC
      end

      let(:expected_app_details_array) do
        [
          ["497799835", "Xcode"],
          ["425424353", "The Unarchiver"],
          ["08981434", "iMovie"],
          ["409201541", "Pages"],
          ["123456789", "123AppNameWithNumbers"],
          ["409203825", "Numbers"],
          ["944924917", "Pastebin It!"],
          ["123456789", "My (cool) app"],
          ["987654321", "an-app-i-use"],
          ["123457867", "App name with many spaces"],
          ["893489734", "my,comma,app"],
          ["832423434", "another_app_name"],
          ["543213432", "My App?"],
          ["688963445", "app;with;semicolons"],
          ["123345384", "my 😊 app"],
          ["896732467", "你好"],
          ["634324555", "مرحبا"],
          ["234324325", "áéíóú"],
          ["310633997", "non><printing><characters"],
        ]
      end

      let(:expected_mas_dumped_output) do
        <<~HEREDOC
          mas "123AppNameWithNumbers", id: 123456789
          mas "an-app-i-use", id: 987654321
          mas "another_app_name", id: 832423434
          mas "App name with many spaces", id: 123457867
          mas "app;with;semicolons", id: 688963445
          mas "iMovie", id: 08981434
          mas "My (cool) app", id: 123456789
          mas "My App?", id: 543213432
          mas "my 😊 app", id: 123345384
          mas "my,comma,app", id: 893489734
          mas "non><printing><characters", id: 310633997
          mas "Numbers", id: 409203825
          mas "Pages", id: 409201541
          mas "Pastebin It!", id: 944924917
          mas "The Unarchiver", id: 425424353
          mas "Xcode", id: 497799835
          mas "áéíóú", id: 234324325
          mas "مرحبا", id: 634324555
          mas "你好", id: 896732467
        HEREDOC
      end

      before do
        described_class.reset!
        allow(described_class).to receive_messages(package_manager_executable: Pathname.new("mas"),
                                                   "`":                        invalid_mas_output)
      end

      specify do
        expect(dumper.apps).to eql(expected_app_details_array)
        expect(dumper.dump).to eq(expected_mas_dumped_output.strip)
      end
    end

    context "with the new format after mas-cli/mas#339" do
      let(:new_mas_output) do
        <<~HEREDOC
          1440147259  AdGuard for Safari  (1.9.13)
          497799835   Xcode               (12.5)
          425424353   The Unarchiver      (4.3.1)
        HEREDOC
      end

      let(:expected_app_details_array) do
        [
          ["1440147259", "AdGuard for Safari"],
          ["497799835", "Xcode"],
          ["425424353", "The Unarchiver"],
        ]
      end

      before do
        described_class.reset!
        allow(described_class).to receive_messages(package_manager_executable: Pathname.new("mas"),
                                                   "`":                        new_mas_output)
      end

      it "parses the app names without trailing whitespace" do
        expect(dumper.apps).to eql(expected_app_details_array)
      end
    end
  end

  describe "installing" do
    before do
      stub_formula_loader formula("mas") {
        T.bind(self, T.class_of(Formula))
        url "mas-1.0"
      }
    end

    describe ".installed_app_ids" do
      it "shells out" do
        expect { described_class.installed_app_ids }.not_to raise_error
      end
    end

    describe ".app_id_installed_and_up_to_date?" do
      it "returns result" do
        allow(described_class).to receive_messages(installed_app_ids: [123, 456],
                                                   outdated_app_ids:  [456])
        expect(described_class.app_id_installed_and_up_to_date?(123)).to be(true)
        expect(described_class.app_id_installed_and_up_to_date?(456)).to be(false)
      end
    end

    context "when mas is not installed" do
      before do
        allow(described_class).to receive(:package_manager_executable).and_return(nil)
      end

      it "tries to install mas" do
        expect(Homebrew::Bundle).to receive(:system).with(HOMEBREW_BREW_FILE, "install", "mas",
                                                          verbose: false).and_return(true)
        expect { described_class.preinstall!("foo", 123) }.to raise_error(RuntimeError)
      end

      describe ".outdated_app_ids" do
        it "does not shell out" do
          expect(described_class).not_to receive(:`)
          described_class.reset!
          described_class.outdated_app_ids
        end
      end
    end

    context "when mas is installed" do
      before do
        allow(described_class).to receive(:package_manager_executable).and_return(Pathname.new("mas"))
      end

      describe ".outdated_app_ids" do
        it "returns app ids" do
          expect(described_class).to receive(:`).and_return("foo 123")
          described_class.reset!
          described_class.outdated_app_ids
        end
      end

      context "when app is installed" do
        before do
          allow(described_class).to receive(:installed_app_ids).and_return([123])
        end

        it "skips" do
          expect(Homebrew::Bundle).not_to receive(:system)
          expect(described_class.preinstall!("foo", 123)).to be(false)
        end
      end

      context "when app is outdated" do
        before do
          allow(described_class).to receive_messages(installed_app_ids: [123], outdated_app_ids: [123])
        end

        it "upgrades" do
          expect(Homebrew::Bundle).to receive(:system).with(Pathname("mas"), "upgrade", "123", verbose: false)
                                                      .and_return(true)
          expect(described_class.preinstall!("foo", 123)).to be(true)
          expect(described_class.install!("foo", 123)).to be(true)
        end
      end

      context "when app is not installed" do
        before do
          allow(described_class).to receive(:installed_app_ids).and_return([])
        end

        it "installs app" do
          expect(Homebrew::Bundle).to receive(:system).with(Pathname("mas"), "install", "123", verbose: false)
                                                      .and_return(true)
          expect(described_class.preinstall!("foo", 123)).to be(true)
          expect(described_class.install!("foo", 123)).to be(true)
        end

        it "falls back to `mas get` when `mas install` fails" do
          expect(Homebrew::Bundle).to receive(:system).with(Pathname("mas"), "install", "123", verbose: false)
                                                      .and_return(false)
          expect(Homebrew::Bundle).to receive(:system).with(Pathname("mas"), "get", "123", verbose: false)
                                                      .and_return(true)
          expect(described_class.preinstall!("foo", 123)).to be(true)
          expect(described_class.install!("foo", 123)).to be(true)
        end
      end
    end
  end

  describe "cleanup" do
    before do
      described_class.reset!
      allow(described_class).to receive_messages(package_manager_executable: Pathname.new("mas"), packages: [
        Homebrew::Bundle::MacAppStore::App.new(id: "123", name: "foo"),
        Homebrew::Bundle::MacAppStore::App.new(id: "456", name: "bar"),
        Homebrew::Bundle::MacAppStore::App.new(id: "0", name: "testflight"),
      ])
    end

    it "returns apps not in Brewfile entries by ID" do
      entries = [Homebrew::Bundle::Dsl::Entry.new(:mas, "renamed foo", id: 123)]
      items = described_class.cleanup_items(entries)

      expect(items.map { |item| described_class.cleanup_item_name(item) }).to eql(["bar (456)"])
    end

    it "uninstalls apps by ID" do
      items = described_class.cleanup_items([Homebrew::Bundle::Dsl::Entry.new(:mas, "foo", id: 123)])
      expect(Homebrew::Bundle).to receive(:system).with(Pathname("mas"), "uninstall", "456", verbose: false)
                                                  .and_return(true)

      expect { described_class.cleanup!(items) }.to output(/Uninstalled 1 Mac App Store app/).to_stdout
    end
  end
end
