require "spec_helper"

module Omnibus
  describe NetFetcher do
    let(:root_prefix) { "" }
    let(:project_dir) { "#{root_prefix}/tmp/project" }
    let(:build_dir) { "#{root_prefix}/tmp/build" }
    let(:source) do
      { url: "https://get.example.com/file.tar.gz", md5: "abcd1234" }
    end

    let(:manifest_entry) do
      double(Omnibus::ManifestEntry,
        name: "file",
        locked_version: "1.2.3",
        described_version: "1.2.3",
        locked_source: source)
    end

    let(:cache_dir) { "/cache" }

    before do
      Config.cache_dir(cache_dir)
    end

    subject { described_class.new(manifest_entry, project_dir, build_dir) }

    describe "authorization" do
      context "when none passed" do
        it "does not get passed" do
          expect(subject).to receive(:download_file!) do |url_arg, path_arg, options_arg|
            expect(options_arg).to_not have_key("Authorization")
          end

          subject.send(:download)
        end
      end

      context "when passed" do
        let(:auth_header) { "a fake auth header" }
        let(:source) do
          { url: "https://get.example.com/file.tar.gz", md5: "abcd1234", authorization: auth_header }
        end

        it "does get passed" do
          expect(subject).to receive(:download_file!) do |url_arg, path_arg, options_arg|
            expect(options_arg).to have_key("Authorization")
            expect(options_arg["Authorization"]).to eq(auth_header)
          end

          subject.send(:download)
        end
      end
    end

    describe "#fetch_required?" do
      context "when file is not downloaded" do
        before { allow(File).to receive(:exist?).and_return(false) }

        it "returns true" do
          expect(subject.fetch_required?).to be_truthy
        end
      end

      context "when the file is downloaded" do
        before { allow(File).to receive(:exist?).and_return(true) }

        context "when the shasums differ" do
          before do
            allow(subject).to receive(:digest).and_return("abcd1234")
            allow(subject).to receive(:checksum).and_return("efgh5678")
          end

          it "returns true" do
            expect(subject.fetch_required?).to be_truthy
          end
        end

        context "when the shasums are the same" do
          before do
            allow(subject).to receive(:digest).and_return("abcd1234")
            allow(subject).to receive(:checksum).and_return("abcd1234")
          end

          it "returns true" do
            expect(subject.fetch_required?).to be(false)
          end
        end
      end
    end

    describe "#version_guid" do
      context "source with md5" do
        it "returns the shasum" do
          expect(subject.version_guid).to eq("md5:abcd1234")
        end
      end

      context "source with sha1" do
        let(:source) do
          { url: "https://get.example.com/file.tar.gz", sha1: "abcd1234" }
        end

        it "returns the shasum" do
          expect(subject.version_guid).to eq("sha1:abcd1234")
        end
      end

      context "source with sha256" do
        let(:source) do
          { url: "https://get.example.com/file.tar.gz", sha256: "abcd1234" }
        end

        it "returns the shasum" do
          expect(subject.version_guid).to eq("sha256:abcd1234")
        end
      end

      context "source with sha512" do
        let(:source) do
          { url: "https://get.example.com/file.tar.gz", sha512: "abcd1234" }
        end

        it "returns the shasum" do
          expect(subject.version_guid).to eq("sha512:abcd1234")
        end
      end
    end

    describe "#clean" do
      before do
        allow(FileUtils).to receive(:rm_rf)
        allow(subject).to receive(:deploy)
        allow(subject).to receive(:create_required_directories)
      end

      context "when the project directory exists" do
        before { allow(File).to receive(:exist?).and_return(true) }

        it "deploys the archive" do
          expect(subject).to receive(:deploy)
          subject.clean
        end

        it "returns true" do
          expect(subject.clean).to be_truthy
        end

        it "removes the project directory" do
          expect(FileUtils).to receive(:rm_rf).with(project_dir)
          subject.clean
        end
      end

      context "when the project directory does not exist" do
        before { allow(File).to receive(:exist?).and_return(false) }

        it "deploys the archive" do
          expect(subject).to receive(:deploy)
          subject.clean
        end

        it "returns false" do
          expect(subject.clean).to be(false)
        end
      end
    end

    describe "#version_for_cache" do
      context "source with md5" do
        it "returns the download URL and md5" do
          expect(subject.version_for_cache).to eq("download_url:https://get.example.com/file.tar.gz|md5:abcd1234")
        end
      end

      context "source with sha1" do
        let(:source) do
          { url: "https://get.example.com/file.tar.gz", sha1: "abcd1234" }
        end

        it "returns the download URL and sha1" do
          expect(subject.version_for_cache).to eq("download_url:https://get.example.com/file.tar.gz|sha1:abcd1234")
        end
      end

      context "source with sha256" do
        let(:source) do
          { url: "https://get.example.com/file.tar.gz", sha256: "abcd1234" }
        end

        it "returns the download URL and sha256" do
          expect(subject.version_for_cache).to eq("download_url:https://get.example.com/file.tar.gz|sha256:abcd1234")
        end
      end

      context "source with sha512" do
        let(:source) do
          { url: "https://get.example.com/file.tar.gz", sha512: "abcd1234" }
        end

        it "returns the download URL and sha1" do
          expect(subject.version_for_cache).to eq("download_url:https://get.example.com/file.tar.gz|sha512:abcd1234")
        end
      end
    end

    describe "#download_url" do
      context "s3 cache enabled" do
        before do
          Config.use_s3_caching(true)
          Config.s3_access_key("ABCD1234")
          Config.s3_secret_key("EFGH5678")
          Config.s3_bucket("mybucket")

          # clear cache
          S3Cache.remove_instance_variable(:@s3_client) if S3Cache.instance_variable_defined?(:@s3_client)
        end

        it "returns the source s3 generated url" do
          expect(subject.send(:download_url)).to eq("https://mybucket.s3.amazonaws.com/file-1.2.3-abcd1234")
        end

        context "custom endpoint" do
          before { Config.s3_endpoint("http://example.com") }

          it "returns the url using custom s3 endpoint" do
            expect(subject.send(:download_url)).to eq("http://mybucket.example.com/file-1.2.3-abcd1234")
          end
        end

        context "custom endpoint with path style urls" do
          before do
            Config.s3_force_path_style(true)
            Config.s3_endpoint("http://example.com")
          end

          it "returns the url using path style" do
            expect(subject.send(:download_url)).to eq("http://example.com/mybucket/file-1.2.3-abcd1234")
          end
        end

        context "s3 transfer acceleration" do
          before { Config.s3_accelerate(true) }

          it "returns the url using s3 accelerate endpoint" do
            expect(subject.send(:download_url)).to eq("https://mybucket.s3-accelerate.amazonaws.com/file-1.2.3-abcd1234")
          end
        end
      end

      context "s3 cache disabled" do
        it "returns the source url" do
          expect(subject.send(:download_url)).to eq("https://get.example.com/file.tar.gz")
        end
      end
    end

    describe "downloading the file" do

      let(:expected_open_opts) do
        {
          "Accept-Encoding" => "identity",
          :read_timeout => 60,
          :content_length_proc => an_instance_of(Proc),
          :progress_proc => an_instance_of(Proc),
        }
      end

      let(:tempfile_path) { "/tmp/intermediate_path/tempfile_path.random_garbage.tmp" }

      let(:fetched_file) { instance_double("TempFile", path: tempfile_path) }

      let(:destination_path) { "/cache/file.tar.gz" }

      let(:progress_bar_output) { StringIO.new }

      let(:reported_content_length) { 100 }

      let(:cumulative_downloaded_length) { 100 }

      let(:uri_open_target) do
        RUBY_VERSION.to_f < 2.7 ? subject : URI
      end

      def capturing_stdout
        old_stdout, $stdout = $stdout, progress_bar_output
        yield
      ensure
        $stdout = old_stdout
      end

      before do
        expect(uri_open_target).to receive(:open).with(source[:url], expected_open_opts) do |_url, open_uri_opts|
          open_uri_opts[:content_length_proc].call(reported_content_length)
          open_uri_opts[:progress_proc].call(cumulative_downloaded_length)

          fetched_file
        end
        expect(fetched_file).to receive(:close)
        expect(FileUtils).to receive(:cp).with(tempfile_path, destination_path)
        expect(fetched_file).to receive(:unlink)
      end

      it "downloads the thing" do
        capturing_stdout do
          expect { subject.send(:download) }.to_not raise_error
        end
      end

      # In Ci we somewhat frequently see:
      #   ProgressBar::InvalidProgressError: You can't set the item's current value to be greater than the total.
      #
      # My hunch is that this is caused by some floating point shenanigans
      # where we sum a bunch of floating point numbers and they add up to some
      # small fraction greater than the actual total. Since we're gonna verify
      # the checksum of what we downloaded later, we don't want to hear about
      # this error.
      context "when cumulative downloaded amount exceeds reported content length" do

        let(:reported_content_length) { 100 }

        let(:cumulative_downloaded_length) { 100.1 }

        it "downloads the thing" do
          capturing_stdout do
            expect { subject.send(:download) }.to_not raise_error
          end
        end

      end

    end

    shared_examples "an extractor" do |extension, source_options, commands|
      context "when the file is a .#{extension}" do
        let(:manifest_entry) do
          double(Omnibus::ManifestEntry,
            name: "file",
            locked_version: "1.2.3",
            described_version: "1.2.3",
            locked_source: { url: "https://get.example.com/file.#{extension}", md5: "abcd1234" }.merge(source_options))
        end

        subject { described_class.new(manifest_entry, project_dir, build_dir) }

        it "shells out with the right commands" do
          commands.each do |command|
            if command.is_a?(String)
              expect(subject).to receive(:shellout!).with(command)
            else
              expect(subject).to receive(:shellout!).with(*command)
            end
          end
          subject.send(:extract)
        end
      end
    end

    describe "#deploy" do
      before do
        described_class.send(:public, :deploy)
      end

      context "when the downloaded file is a folder" do
        let(:manifest_entry) do
          double(Omnibus::ManifestEntry,
            name: "file",
            locked_version: "1.2.3",
            described_version: "1.2.3",
            locked_source: { url: "https://get.example.com/folder", md5: "abcd1234" })
        end

        subject { described_class.new(manifest_entry, project_dir, build_dir) }

        before do
          allow(File).to receive(:directory?).and_return(true)
        end

        it "copies the entire directory to project_dir" do
          expect(FileUtils).to receive(:cp_r).with("#{cache_dir}/folder/.", project_dir)
          subject.deploy
        end
      end

      context "when the downloaded file is a regular file" do
        let(:manifest_entry) do
          double(Omnibus::ManifestEntry,
            name: "file",
            locked_version: "1.2.3",
            described_version: "1.2.3",
            locked_source: { url: "https://get.example.com/file", md5: "abcd1234" })
        end

        subject { described_class.new(manifest_entry, project_dir, build_dir) }

        before do
          allow(File).to receive(:directory?).and_return(false)
        end

        it "copies the file into the project_dir" do
          expect(FileUtils).to receive(:cp).with("#{cache_dir}/file", "#{project_dir}")
          subject.deploy
        end
      end
    end

    describe "#extract" do

      context "on Windows" do
        let(:root_prefix) { "C:" }

        before do
          Config.cache_dir("C:/")
          stub_ohai(platform: "windows", version: "2012R2")
          allow(Dir).to receive(:mktmpdir).and_yield("C:/tmp_dir")
        end

        context "when no extract overrides are present" do
          it_behaves_like "an extractor", "7z", {},
            ['7z.exe x C:\\file.7z -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "zip", {},
            ['7z.exe x C:\\file.zip -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "tar", {},
            [["tar xf C:/file.tar --force-local -CC:/tmp/project", { returns: [0] }]]
          it_behaves_like "an extractor", "tgz", {},
            [["tar zxf C:/file.tgz --force-local -CC:/tmp/project", { returns: [0] }]]
          it_behaves_like "an extractor", "tar.gz", {},
            [["tar zxf C:/file.tar.gz --force-local -CC:/tmp/project", { returns: [0] }]]
          it_behaves_like "an extractor", "tar.bz2", {},
            [["tar jxf C:/file.tar.bz2 --force-local -CC:/tmp/project", { returns: [0] }]]
          it_behaves_like "an extractor", "txz", {},
            [["tar Jxf C:/file.txz --force-local -CC:/tmp/project", { returns: [0] }]]
          it_behaves_like "an extractor", "tar.xz", {},
            [["tar Jxf C:/file.tar.xz --force-local -CC:/tmp/project", { returns: [0] }]]
          it_behaves_like "an extractor", "tar.lzma", {},
            [["tar --lzma -xf C:/file.tar.lzma --force-local -CC:/tmp/project", { returns: [0] }]]
        end

        context "when seven_zip extract strategy is chosen" do
          it_behaves_like "an extractor", "7z", { extract: :seven_zip },
            ['7z.exe x C:\\file.7z -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "zip", { extract: :seven_zip },
            ['7z.exe x C:\\file.zip -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "tar", { extract: :seven_zip },
            ['7z.exe x C:\\file.tar -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "tgz", { extract: :seven_zip },
            ['7z.exe x C:\\file.tgz -oC:\\tmp_dir -r -y',
             '7z.exe x C:\\tmp_dir\\file.tar -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "tar.gz", { extract: :seven_zip },
            ['7z.exe x C:\\file.tar.gz -oC:\\tmp_dir -r -y',
             '7z.exe x C:\\tmp_dir\\file.tar -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "tar.bz2", { extract: :seven_zip },
            ['7z.exe x C:\\file.tar.bz2 -oC:\\tmp_dir -r -y',
             '7z.exe x C:\\tmp_dir\\file.tar -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "txz", { extract: :seven_zip },
            ['7z.exe x C:\\file.txz -oC:\\tmp_dir -r -y',
             '7z.exe x C:\\tmp_dir\\file.tar -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "tar.xz", { extract: :seven_zip },
            ['7z.exe x C:\\file.tar.xz -oC:\\tmp_dir -r -y',
             '7z.exe x C:\\tmp_dir\\file.tar -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "tar.lzma", { extract: :seven_zip },
            ['7z.exe x C:\\file.tar.lzma -oC:\\tmp_dir -r -y',
             '7z.exe x C:\\tmp_dir\\file.tar -oC:\\tmp\\project -r -y']
        end

        context "when lax_tar extract strategy is chosen" do
          it_behaves_like "an extractor", "7z", { extract: :lax_tar },
            ['7z.exe x C:\\file.7z -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "zip", { extract: :lax_tar },
            ['7z.exe x C:\\file.zip -oC:\\tmp\\project -r -y']
          it_behaves_like "an extractor", "tar", { extract: :lax_tar },
            [["tar xf C:/file.tar --force-local -CC:/tmp/project", { returns: [0, 1] }]]
          it_behaves_like "an extractor", "tgz", { extract: :lax_tar },
            [["tar zxf C:/file.tgz --force-local -CC:/tmp/project", { returns: [0, 1] }]]
          it_behaves_like "an extractor", "tar.gz", { extract: :lax_tar },
            [["tar zxf C:/file.tar.gz --force-local -CC:/tmp/project", { returns: [0, 1] }]]
          it_behaves_like "an extractor", "tar.bz2", { extract: :lax_tar },
            [["tar jxf C:/file.tar.bz2 --force-local -CC:/tmp/project", { returns: [0, 1] }]]
          it_behaves_like "an extractor", "txz", { extract: :lax_tar },
            [["tar Jxf C:/file.txz --force-local -CC:/tmp/project", { returns: [0, 1] }]]
          it_behaves_like "an extractor", "tar.xz", { extract: :lax_tar },
            [["tar Jxf C:/file.tar.xz --force-local -CC:/tmp/project", { returns: [0, 1] }]]
          it_behaves_like "an extractor", "tar.lzma", { extract: :lax_tar },
            [["tar --lzma -xf C:/file.tar.lzma --force-local -CC:/tmp/project", { returns: [0, 1] }]]
        end
      end

      context "on Linux" do
        before do
          Config.cache_dir("/")
          stub_ohai(platform: "ubuntu", version: "16.04")
          stub_const("File::ALT_SEPARATOR", nil)

          allow(Omnibus).to receive(:which)
            .with("gtar")
            .and_return(false)
        end

        context "when gtar is not present" do
          it_behaves_like "an extractor", "7z", {},
            ["7z x /file.7z -o/tmp/project -r -y"]
          it_behaves_like "an extractor", "zip", {},
            ["unzip /file.zip -d /tmp/project"]
          it_behaves_like "an extractor", "tar", {},
            ["tar xf /file.tar -C/tmp/project"]
          it_behaves_like "an extractor", "tgz", {},
            ["tar zxf /file.tgz -C/tmp/project"]
          it_behaves_like "an extractor", "tar.gz", {},
            ["tar zxf /file.tar.gz -C/tmp/project"]
          it_behaves_like "an extractor", "tar.bz2", {},
            ["tar jxf /file.tar.bz2 -C/tmp/project"]
          it_behaves_like "an extractor", "txz", {},
            ["tar Jxf /file.txz -C/tmp/project"]
          it_behaves_like "an extractor", "tar.xz", {},
            ["tar Jxf /file.tar.xz -C/tmp/project"]
          it_behaves_like "an extractor", "tar.lzma", {},
            ["tar --lzma -xf /file.tar.lzma -C/tmp/project"]
        end

        context "when gtar is present" do
          before do
            Config.cache_dir("/")

            stub_ohai(platform: "ubuntu", version: "16.04")
            stub_const("File::ALT_SEPARATOR", nil)

            allow(Omnibus).to receive(:which)
              .with("gtar")
              .and_return("/path/to/gtar")
          end

          it_behaves_like "an extractor", "7z", {},
            ["7z x /file.7z -o/tmp/project -r -y"]
          it_behaves_like "an extractor", "zip", {},
            ["unzip /file.zip -d /tmp/project"]
          it_behaves_like "an extractor", "tar", {},
            ["gtar xf /file.tar -C/tmp/project"]
          it_behaves_like "an extractor", "tgz", {},
            ["gtar zxf /file.tgz -C/tmp/project"]
          it_behaves_like "an extractor", "tar.gz", {},
            ["gtar zxf /file.tar.gz -C/tmp/project"]
          it_behaves_like "an extractor", "tar.bz2", {},
            ["gtar jxf /file.tar.bz2 -C/tmp/project"]
          it_behaves_like "an extractor", "txz", {},
            ["gtar Jxf /file.txz -C/tmp/project"]
          it_behaves_like "an extractor", "tar.xz", {},
            ["gtar Jxf /file.tar.xz -C/tmp/project"]
          it_behaves_like "an extractor", "tar.lzma", {},
            ["gtar --lzma -xf /file.tar.lzma -C/tmp/project"]
        end

      end
    end
  end
end
