require 'spec_helper'
require 'puppet/face'
require 'puppet/interface'

describe Puppet::Interface do
  subject { Puppet::Interface }

  before :each do
    @faces = Puppet::Interface::FaceCollection.
      instance_variable_get("@faces").dup
    @dq = $".dup
    $".delete_if do |path| path =~ %r{/face/.*\.rb$} end
    Puppet::Interface::FaceCollection.instance_variable_get("@faces").clear
  end

  after :each do
    Puppet::Interface::FaceCollection.instance_variable_set("@faces", @faces)
    $".clear ; @dq.each do |item| $" << item end
  end

  describe "#[]" do
    it "should fail when no version is requested" do
      expect { subject[:huzzah] }.to raise_error ArgumentError
    end

    it "should raise an exception when the requested version is unavailable" do
      expect { subject[:huzzah, '17.0.0'] }.to raise_error(Puppet::Error, /Could not find version/)
    end

    it "should raise an exception when the requested face doesn't exist" do
      expect { subject[:burrble_toot, :current] }.to raise_error(Puppet::Error, /Could not find Puppet Face/)
    end

    describe "version matching" do
      { '1'     => '1.1.1',
        '1.0'   => '1.0.1',
        '1.0.1' => '1.0.1',
        '1.1'   => '1.1.1',
        '1.1.1' => '1.1.1'
      }.each do |input, expect|
        it "should match #{input.inspect} to #{expect.inspect}" do
          face = subject[:version_matching, input]
          expect(face).to be
          expect(face.version).to eq(expect)
        end
      end

      %w{1.0.2 1.2}.each do |input|
        it "should not match #{input.inspect} to any version" do
          expect { subject[:version_matching, input] }.
            to raise_error Puppet::Error, /Could not find version/
        end
      end
    end
  end

  describe "#define" do
    it "should register the face" do
      face  = subject.define(:face_test_register, '0.0.1')
      expect(face).to eq(subject[:face_test_register, '0.0.1'])
    end

    it "should load actions" do
      expect_any_instance_of(subject).to receive(:load_actions)
      subject.define(:face_test_load_actions, '0.0.1')
    end

    it "should require a version number" do
      expect { subject.define(:no_version) }.to raise_error ArgumentError
    end

    it "should support summary builder and accessor methods" do
      expect(subject.new(:foo, '1.0.0')).to respond_to(:summary).with(0).arguments
      expect(subject.new(:foo, '1.0.0')).to respond_to(:summary=).with(1).arguments
    end

    # Required documentation methods...
    { :summary     => "summary",
      :description => "This is the description of the stuff\n\nWhee",
      :examples    => "This is my example",
      :short_description => "This is my custom short description",
      :notes       => "These are my notes...",
      :author      => "This is my authorship data",
    }.each do |attr, value|
      it "should support #{attr} in the builder" do
        face = subject.new(:builder, '1.0.0') do
          self.send(attr, value)
        end
        expect(face.send(attr)).to eq(value)
      end
    end
  end

  describe "#initialize" do
    it "should require a version number" do
      expect { subject.new(:no_version) }.to raise_error ArgumentError
    end

    it "should require a valid version number" do
      expect { subject.new(:bad_version, 'Rasins') }.
        to raise_error ArgumentError
    end

    it "should instance-eval any provided block" do
      face = subject.new(:face_test_block, '0.0.1') do
        action(:something) do
          when_invoked {|_| "foo" }
        end
      end

      expect(face.something).to eq("foo")
    end
  end

  it "should have a name" do
    expect(subject.new(:me, '0.0.1').name).to eq(:me)
  end

  it "should stringify with its own name" do
    expect(subject.new(:me, '0.0.1').to_s).to match(/\bme\b/)
  end

  it "should try to require faces that are not known" do
    expect(subject::FaceCollection).to receive(:load_face).with(:foo, :current)
    expect(subject::FaceCollection).to receive(:load_face).with(:foo, '0.0.1')
    expect { subject[:foo, '0.0.1'] }.to raise_error Puppet::Error
  end

  describe 'when raising NoMethodErrors' do
    subject { described_class.new(:foo, '1.0.0') }

    if RUBY_VERSION.to_f >= 3.3
      it 'includes the face name in the error message' do
        expect { subject.boombaz }.to raise_error(NoMethodError, /for an instance of Puppet::Interface/)
      end

      it 'includes the face version in the error message' do
        expect { subject.boombaz }.to raise_error(NoMethodError, /for an instance of Puppet::Interface/)
      end
    else
      it 'includes the face name in the error message' do
        expect { subject.boombaz }.to raise_error(NoMethodError, /#{subject.name}/)
      end

      it 'includes the face version in the error message' do
        expect { subject.boombaz }.to raise_error(NoMethodError, /#{subject.version}/)
      end
    end
  end

  it_should_behave_like "things that declare options" do
    def add_options_to(&block)
      subject.new(:with_options, '0.0.1', &block)
    end
  end

  context "when deprecating a face" do
    let(:face) { subject.new(:foo, '0.0.1') }
    describe "#deprecate" do
      it "should respond to #deprecate" do
        expect(subject.new(:foo, '0.0.1')).to respond_to(:deprecate)
      end

      it "should set the deprecated value to true" do
        expect(face.deprecated?).to be_falsey
        face.deprecate
        expect(face.deprecated?).to be_truthy
      end
    end

    describe "#deprecated?" do
      it "should return a nil (falsey) value by default" do
        expect(face.deprecated?).to be_falsey
      end

      it "should return true if the face has been deprecated" do
        expect(face.deprecated?).to be_falsey
        face.deprecate
        expect(face.deprecated?).to be_truthy
      end
    end
  end

  describe "with face-level display_global_options" do
    it "should not return any action level display_global_options" do
      face = subject.new(:with_display_global_options, '0.0.1') do
        display_global_options "environment"
        action :baz do
          when_invoked {|_| true }
          display_global_options "modulepath"
        end
      end
      expect(face.display_global_options).to match(["environment"])
    end

    it "should not fail when a face d_g_o duplicates an action d_g_o" do
      expect {
        subject.new(:action_level_display_global_options, '0.0.1') do
          action :bar do
            when_invoked {|_| true }
            display_global_options "environment"
          end
          display_global_options "environment"
        end
      }.to_not raise_error
    end

    it "should work when two actions have the same d_g_o" do
      face = subject.new(:with_display_global_options, '0.0.1') do
        action :foo do when_invoked {|_| true} ; display_global_options "environment" end
        action :bar do when_invoked {|_| true} ; display_global_options "environment" end
      end
      expect(face.get_action(:foo).display_global_options).to match(["environment"])
      expect(face.get_action(:bar).display_global_options).to match(["environment"])
    end
      
  end
  
  describe "with inherited display_global_options" do
  end

  describe "with face-level options" do
    it "should not return any action-level options" do
      face = subject.new(:with_options, '0.0.1') do
        option "--foo"
        option "--bar"
        action :baz do
          when_invoked {|_| true }
          option "--quux"
        end
      end
      expect(face.options).to match_array([:foo, :bar])
    end

    it "should fail when a face option duplicates an action option" do
      expect {
        subject.new(:action_level_options, '0.0.1') do
          action :bar do
            when_invoked {|_| true }
            option "--foo"
          end
          option "--foo"
        end
      }.to raise_error ArgumentError, /Option foo conflicts with existing option foo on/i
    end

    it "should work when two actions have the same option" do
      face = subject.new(:with_options, '0.0.1') do
        action :foo do when_invoked {|_| true } ; option "--quux" end
        action :bar do when_invoked {|_| true } ; option "--quux" end
      end

      expect(face.get_action(:foo).options).to match_array([:quux])
      expect(face.get_action(:bar).options).to match_array([:quux])
    end

    it "should only list options and not aliases" do
      face = subject.new(:face_options, '0.0.1') do
        option "--bar", "-b", "--foo-bar"
      end
      expect(face.options).to match_array([:bar])
    end

  end

  describe "with inherited options" do
    let :parent do
      parent = Class.new(subject)
      parent.option("--inherited")
      parent.action(:parent_action) do when_invoked {|_| true } end
      parent
    end

    let :face do
      face = parent.new(:example, '0.2.1')
      face.option("--local")
      face.action(:face_action) do when_invoked {|_| true } end
      face
    end

    describe "#options" do
      it "should list inherited options" do
        expect(face.options).to match_array([:inherited, :local])
      end

      it "should see all options on face actions" do
        expect(face.get_action(:face_action).options).to match_array([:inherited, :local])
      end

      it "should see all options on inherited actions accessed on the subclass" do
        expect(face.get_action(:parent_action).options).to match_array([:inherited, :local])
      end

      it "should not see subclass actions on the parent class" do
        expect(parent.options).to match_array([:inherited])
      end

      it "should not see subclass actions on actions accessed on the parent class" do
        expect(parent.get_action(:parent_action).options).to match_array([:inherited])
      end
    end

    describe "#get_option" do
      it "should return an inherited option object" do
        expect(face.get_option(:inherited)).to be_an_instance_of subject::Option
      end
    end
  end

  it_should_behave_like "documentation on faces" do
    subject do
      Puppet::Interface.new(:face_documentation, '0.0.1')
    end
  end
end
