# frozen_string_literal: true

require 'spec_helper'
require 'puppet-strings/yard'

describe PuppetStrings::Yard::Handlers::Ruby::TypeHandler do
  subject {
    YARD::Parser::SourceParser.parse_string(source, :ruby)
    YARD::Registry.all(:puppet_type)
  }

  describe 'parsing source without a type definition' do
    let(:source) { 'puts "hi"' }

    it 'no types should be in the registry' do
      expect(subject.empty?).to eq(true)
    end
  end

  describe 'parsing a type with a missing description' do
    let(:source) { <<-SOURCE
Puppet::Type.newtype(:database) do
end
    SOURCE
    }

    it 'should log a warning' do
      expect{ subject }.to output(/\[warn\]: Missing a description for Puppet resource type 'database' at \(stdin\):1\./).to_stdout_from_any_process
    end
  end

  describe 'parsing a type with an invalid docstring assignment' do
    let(:source) { <<-SOURCE
Puppet::Type.newtype(:database) do
  @doc = 123
end
    SOURCE
    }

    it 'should log an error' do
      expect { subject }.to output(/Failed to parse docstring/).to_stdout_from_any_process
    end
  end

  describe 'parsing a type with a valid docstring assignment' do
    let(:source) { <<-SOURCE
Puppet::Type.newtype(:database) do
  @doc = 'An example database server resource type.'
end
    SOURCE
    }

    it 'should correctly detect the docstring' do
      expect(subject.size).to eq(1)
      object = subject.first
      expect(object.docstring).to eq('An example database server resource type.')
    end
  end

  describe 'parsing a type with a docstring which uses ruby `%Q` notation' do
    let(:source) { <<-'SOURCE'
Puppet::Type.newtype(:database) do
  test = 'hello world!'
  desc %Q{This is a multi-line
  doc in %Q with #{test}}
end
    SOURCE
    }

    it 'should strip the `%Q{}` and render the interpolation expression literally' do
      expect(subject.size).to eq(1)
      object = subject.first
      expect(object.docstring).to eq("This is a multi-line\ndoc in %Q with \#{test}")
    end
  end

  describe 'parsing a type with a param with arguments' do
    let(:source) { <<-SOURCE
Puppet::Type.newtype(:database) do
  feature :encryption, 'The provider supports encryption.', methods: [:encrypt]

  newparam(:encryption_key, :parent => Puppet::Parameter::Boolean, required_features: :encryption) do
    desc 'The encryption key to use.'
    defaultto false
  end
end
    SOURCE
    }

    it 'should correctly detect the required_feature' do
      expect(subject.size).to eq(1)
      object = subject.first
      expect(object.parameters[0].required_features).to eq('encryption')
    end

    it 'should correctly detect a boolean parent' do
      expect(subject.size).to eq(1)
      object = subject.first
      expect(object.parameters[0].default).to eq('false')
    end
  end

  describe 'parsing a type definition' do
    let(:source) { <<-SOURCE
# @!puppet.type.param [value1, value2] dynamic_param Documentation for a dynamic parameter.
# @!puppet.type.property [foo, bar] dynamic_prop Documentation for a dynamic property.
Puppet::Type.newtype(:database) do
  desc 'An example database server resource type.'
  feature :encryption, 'The provider supports encryption.', methods: [:encrypt]

  feature :magic,
    'The feature docstring should have
    whitespace and newlines stripped out.'

  ensurable do
    desc 'What state the database should be in.'
    defaultvalues
    aliasvalue(:up, :present)
    aliasvalue(:down, :absent)
    defaultto :up
  end

  newparam(:address) do
    isnamevar
    desc 'The database server name.'
  end

  newparam(:encryption_key, required_features: :encryption) do
    desc 'The encryption key to use.'
  end

  newparam(:encrypt, :parent => Puppet::Parameter::Boolean) do
    desc 'Whether or not to encrypt the database.'
    defaultto false
  end

  newparam(:backup) do
      desc 'How often to backup the database.'
      defaultto :never
      newvalues(:daily, :monthly, :never)
  end

  newproperty(:file) do
    desc 'The database file to use.'
  end

  newproperty(:log_level) do
    desc 'The log level to use.'
    newvalue(:debug)
    newvalue(:warn)
    newvalue(:error)
    defaultto 'warn'
  end
end
    SOURCE
    }

    it 'should register a type object' do
      expect(subject.size).to eq(1)
      object = subject.first
      expect(object).to be_a(PuppetStrings::Yard::CodeObjects::Type)
      expect(object.namespace).to eq(PuppetStrings::Yard::CodeObjects::Types.instance)
      expect(object.name).to eq(:database)
      expect(object.docstring).to eq('An example database server resource type.')
      expect(object.docstring.tags.size).to eq(1)
      tags = object.docstring.tags(:api)
      expect(tags.size).to eq(1)
      expect(tags[0].text).to eq('public')
      expect(object.properties.size).to eq(4)
      expect(object.properties[0].name).to eq('dynamic_prop')
      expect(object.properties[0].docstring).to eq('Documentation for a dynamic property.')
      expect(object.properties[0].isnamevar).to eq(false)
      expect(object.properties[0].values).to eq(%w(foo bar))
      expect(object.properties[1].name).to eq('ensure')
      expect(object.properties[1].docstring).to eq('What state the database should be in.')
      expect(object.properties[1].isnamevar).to eq(false)
      expect(object.properties[1].default).to eq('up')
      expect(object.properties[1].values).to eq(%w(present absent up down))
      expect(object.properties[1].aliases).to eq({ 'down' => 'absent', 'up' => 'present' })
      expect(object.properties[2].name).to eq('file')
      expect(object.properties[2].docstring).to eq('The database file to use.')
      expect(object.properties[2].isnamevar).to eq(false)
      expect(object.properties[2].default).to be_nil
      expect(object.properties[2].values).to eq([])
      expect(object.properties[2].aliases).to eq({})
      expect(object.properties[3].name).to eq('log_level')
      expect(object.properties[3].docstring).to eq('The log level to use.')
      expect(object.properties[3].isnamevar).to eq(false)
      expect(object.properties[3].default).to eq('warn')
      expect(object.properties[3].values).to eq(%w(debug warn error))
      expect(object.properties[3].aliases).to eq({})
      expect(object.parameters.size).to eq(5)
      expect(object.parameters[0].name).to eq('dynamic_param')
      expect(object.parameters[0].docstring).to eq('Documentation for a dynamic parameter.')
      expect(object.parameters[0].isnamevar).to eq(false)
      expect(object.parameters[0].values).to eq(%w(value1 value2))
      expect(object.parameters[1].name).to eq('address')
      expect(object.parameters[1].docstring).to eq('The database server name.')
      expect(object.parameters[1].isnamevar).to eq(true)
      expect(object.parameters[1].default).to be_nil
      expect(object.parameters[1].values).to eq([])
      expect(object.parameters[1].aliases).to eq({})
      expect(object.parameters[2].name).to eq('encryption_key')
      expect(object.parameters[2].docstring).to eq('The encryption key to use.')
      expect(object.parameters[2].isnamevar).to eq(false)
      expect(object.parameters[2].default).to be_nil
      expect(object.parameters[2].values).to eq([])
      expect(object.parameters[2].aliases).to eq({})
      expect(object.parameters[3].name).to eq('encrypt')
      expect(object.parameters[3].docstring).to eq('Whether or not to encrypt the database.')
      expect(object.parameters[3].isnamevar).to eq(false)
      expect(object.parameters[3].default).to eq('false')
      expect(object.parameters[3].values).to eq(%w(true false yes no))
      expect(object.parameters[3].aliases).to eq({})
      expect(object.parameters[4].name).to eq('backup')
      expect(object.parameters[4].docstring).to eq('How often to backup the database.')
      expect(object.parameters[4].isnamevar).to eq(false)
      expect(object.parameters[4].default).to eq('never')
      expect(object.parameters[4].values).to eq(%w(daily monthly never))
      expect(object.features.size).to eq(2)
      expect(object.features[0].name).to eq('encryption')
      expect(object.features[0].docstring).to eq('The provider supports encryption.')
      expect(object.features[1].name).to eq('magic')
      expect(object.features[1].docstring).to eq('The feature docstring should have whitespace and newlines stripped out.')
    end
  end

  describe 'parsing a valid type with string based name' do
    let(:source) { <<-SOURCE
Puppet::Type.newtype(:'database') do
  desc 'An example database server resource type.'
  ensurable
end
    SOURCE
    }

    it 'should register a type object with default ensure values' do
      expect(subject.size).to eq(1)
      object = subject.first
      expect(object.name).to eq(:database)
    end
  end

  describe 'parsing an ensurable type with default ensure values' do
    let(:source) { <<-SOURCE
Puppet::Type.newtype(:database) do
  desc 'An example database server resource type.'
  ensurable
end
    SOURCE
    }

    it 'should register a type object with default ensure values' do
      expect(subject.size).to eq(1)
      object = subject.first
      expect(object.properties[0].name).to eq('ensure')
      expect(object.properties[0].docstring).to eq('The basic property that the resource should be in.')
      expect(object.properties[0].default).to eq('present')
      expect(object.properties[0].values).to eq(%w(present absent))
    end
  end

  describe 'parsing a type with a parameter with the name of "name"' do
    let(:source) { <<-SOURCE
Puppet::Type.newtype(:database) do
  desc 'An example database server resource type.'
  newparam(:name) do
    desc 'The database server name.'
  end
end
    SOURCE
    }

    it 'should register a type object with the "name" parameter as the namevar' do
      expect(subject.size).to eq(1)
      object = subject.first
      expect(object.parameters.size).to eq(1)
      expect(object.parameters[0].name).to eq('name')
      expect(object.parameters[0].isnamevar).to eq(true)
    end
  end

  describe 'parsing a type with a check with the name of "onlyif"' do
    let(:source) { <<-SOURCE
Puppet::Type.newtype(:testexec) do
  desc 'An example exec type with a check.'
  newcheck(:onlyif) do
    desc 'a test check param'
  end
end
    SOURCE
    }

    it 'should register a check object on the parent type object' do
      expect(subject.size).to eq(1)
      type_object = subject.first
      expect(type_object.checks.size).to eq(1)
      expect(type_object.checks[0].name).to eq('onlyif')
    end
  end

  describe 'parsing a type with a summary' do
    context 'when the summary has fewer than 140 characters' do
      let(:source) { <<-SOURCE
Puppet::Type.newtype(:database) do
  @doc = '@summary A short summary.'
end
      SOURCE
      }

      it 'should parse the summary' do
        expect{ subject }.to output('').to_stdout_from_any_process
        expect(subject.size).to eq(1)
        summary = subject.first.tags(:summary)
        expect(summary.first.text).to eq('A short summary.')
      end
    end

    context 'when the summary has more than 140 characters' do
      let(:source) { <<-SOURCE
Puppet::Type.newtype(:database) do
  @doc = '@summary A short summary that is WAY TOO LONG. AHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH this is not what a summary is for! It should be fewer than 140 characters!!'
end
      SOURCE
      }

      it 'should log a warning' do
        expect{ subject }.to output(/\[warn\]: The length of the summary for puppet_type 'database' exceeds the recommended limit of 140 characters./).to_stdout_from_any_process
      end
    end
  end
end
