require 'r10k/module/forge'
require 'spec_helper'

describe R10K::Module::Forge do
  # TODO: make these *unit* tests not depend on a real module on the real Forge :(

  include_context 'fail on execution'

  let(:fixture_modulepath) { File.expand_path('spec/fixtures/module/forge', PROJECT_ROOT) }
  let(:empty_modulepath) { File.expand_path('spec/fixtures/empty', PROJECT_ROOT) }

  describe "implementing the Puppetfile spec" do
    it "should implement 'branan/eight_hundred', '8.0.0'" do
      expect(described_class).to be_implement('branan/eight_hundred', { version: '8.0.0' })
    end

    it "should implement 'branan-eight_hundred', '8.0.0'" do
      expect(described_class).to be_implement('branan-eight_hundred', { version: '8.0.0' })
    end

    it "should fail with an invalid title" do
      expect(described_class).to_not be_implement('branan!eight_hundred', { version: '8.0.0' })
    end
  end

  describe "implementing the standard options interface" do
    it "should implement {type: forge}" do
      expect(described_class).to be_implement('branan-eight_hundred', {type: 'forge', version: '8.0.0', source: 'not implemented'})
    end
  end

  describe "setting attributes" do
    subject { described_class.new('branan/eight_hundred', '/moduledir', { version: '8.0.0' }) }

    it "sets the name" do
      expect(subject.name).to eq 'eight_hundred'
    end

    it "sets the author" do
      expect(subject.author).to eq 'branan'
    end

    it "sets the dirname" do
      expect(subject.dirname).to eq '/moduledir'
    end

    it "sets the title" do
      expect(subject.title).to eq 'branan-eight_hundred'
    end
  end

  describe "properties" do
    subject { described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' }) }

    it "sets the module type to :forge" do
      expect(subject.properties).to include(:type => :forge)
    end

    it "sets the expected version" do
      expect(subject.properties).to include(:expected => '8.0.0')
    end

    it "sets the actual version" do
      expect(subject).to receive(:current_version).and_return('0.8.0')
      expect(subject.properties).to include(:actual => '0.8.0')
    end
  end

  context "when a module is deprecated" do
    subject { described_class.new('puppetlabs/corosync', fixture_modulepath, { version: :latest }) }

    it "warns on sync if module is not already insync" do
      allow(subject).to receive(:status).and_return(:absent)

      allow(R10K::Forge::ModuleRelease).to receive(:new).and_return(double('mod_release', install: true))

      logger_dbl = double(Log4r::Logger)
      allow_any_instance_of(described_class).to receive(:logger).and_return(logger_dbl)

      allow(logger_dbl).to receive(:info).with(/Deploying module to.*/)
      expect(logger_dbl).to receive(:warn).with(/puppet forge module.*puppetlabs-corosync.*has been deprecated/i)

      subject.sync
    end

    it "does not warn on sync if module is already insync" do
      allow(subject).to receive(:status).and_return(:insync)

      logger_dbl = double(Log4r::Logger)
      allow_any_instance_of(described_class).to receive(:logger).and_return(logger_dbl)

      allow(logger_dbl).to receive(:info).with(/Deploying module to.*/)
      expect(logger_dbl).to_not receive(:warn).with(/puppet forge module.*puppetlabs-corosync.*has been deprecated/i)

      subject.sync
    end
  end

  describe '#expected_version' do
    it "returns an explicitly given expected version" do
      subject = described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' })
      expect(subject.expected_version).to eq '8.0.0'
    end

    it "uses the latest version from the forge when the version is :latest" do
      subject = described_class.new('branan/eight_hundred', fixture_modulepath, { version: :latest })
      expect(subject.v3_module).to receive_message_chain(:current_release, :version).and_return('8.8.8')
      expect(subject.expected_version).to eq '8.8.8'
    end
  end

  describe "determining the status" do

    subject { described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' }) }

    it "is :absent if the module directory is absent" do
      allow(subject).to receive(:exist?).and_return false
      expect(subject.status).to eq :absent
    end

    it "is :mismatched if there is no module metadata" do
      allow(subject).to receive(:exist?).and_return true
      allow(File).to receive(:exist?).and_return false

      expect(subject.status).to eq :mismatched
    end

    it "is :mismatched if module was previously a git checkout" do
      allow(File).to receive(:directory?).and_return true

      expect(subject.status).to eq :mismatched
    end

    it "is :mismatched if the metadata author doesn't match the expected author" do
      allow(subject).to receive(:exist?).and_return true

      allow(subject.instance_variable_get(:@metadata_file)).to receive(:read).and_return subject.metadata
      allow(subject.metadata).to receive(:full_module_name).and_return 'blargh-blargh'

      expect(subject.status).to eq :mismatched
    end

    it "is :outdated if the metadata version doesn't match the expected version" do
      allow(subject).to receive(:exist?).and_return true

      allow(subject.instance_variable_get(:@metadata_file)).to receive(:read).and_return subject.metadata
      allow(subject.metadata).to receive(:version).and_return '7.0.0'
      expect(subject.status).to eq :outdated
    end

    it "is :insync if the version and the author are in sync" do
      allow(subject).to receive(:exist?).and_return true

      expect(subject.status).to eq :insync
    end
  end

  describe "#sync" do
    subject { described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' }) }

    it 'does nothing when the module is in sync' do
      allow(subject).to receive(:status).and_return :insync

      expect(subject).to receive(:install).never
      expect(subject).to receive(:upgrade).never
      expect(subject).to receive(:reinstall).never
      subject.sync
    end

    it 'reinstalls the module when it is mismatched' do
      allow(subject).to receive(:status).and_return :mismatched
      expect(subject).to receive(:reinstall)
      subject.sync
    end

    it 'upgrades the module when it is outdated' do
      allow(subject).to receive(:status).and_return :outdated
      expect(subject).to receive(:upgrade)
      subject.sync
    end

    it 'installs the module when it is absent' do
      allow(subject).to receive(:status).and_return :absent
      expect(subject).to receive(:install)
      subject.sync
    end
  end

  describe '#install' do
    it 'installs the module from the forge' do
      subject = described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' })
      release = instance_double('R10K::Forge::ModuleRelease')
      expect(R10K::Forge::ModuleRelease).to receive(:new).with('branan-eight_hundred', '8.0.0').and_return(release)
      expect(release).to receive(:install).with(subject.path)
      subject.install
    end
  end

  describe '#uninstall' do
    it 'removes the module path' do
      subject = described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' })
      expect(FileUtils).to receive(:rm_rf).with(subject.path.to_s)
      subject.uninstall
    end
  end

  describe '#reinstall' do
    it 'uninstalls and then installs the module' do
      subject = described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' })
      expect(subject).to receive(:uninstall)
      expect(subject).to receive(:install)
      subject.reinstall
    end
  end
end