require 'spec_helper'
require 'puppet/transaction'
require 'puppet_spec/compiler'
require 'matchers/relationship_graph_matchers'
require 'matchers/include_in_order'
require 'matchers/resource'

describe Puppet::Transaction::AdditionalResourceGenerator do
  include PuppetSpec::Compiler
  include PuppetSpec::Files
  include RelationshipGraphMatchers
  include Matchers::Resource

  let(:prioritizer) { Puppet::Graph::SequentialPrioritizer.new }

  def find_vertex(graph, type, title)
    graph.vertices.find {|v| v.type == type and v.title == title}
  end

  Puppet::Type.newtype(:generator) do
    include PuppetSpec::Compiler

    newparam(:name) do
      isnamevar
    end

    newparam(:kind) do
      defaultto :eval_generate
      newvalues(:eval_generate, :generate)
    end

    newparam(:code)

    def respond_to?(method_name)
      method_name == self[:kind] || super
    end

    def eval_generate
      eval_code
    end

    def generate
      eval_code
    end

    def eval_code
      if self[:code]
        compile_to_ral(self[:code]).resources.select { |r| r.ref =~ /Notify/ }
      else
        []
      end
    end
  end

  context "when applying eval_generate" do
    it "should add the generated resources to the catalog" do
      catalog = compile_to_ral(<<-MANIFEST)
        generator { thing:
          code => 'notify { hello: }'
        }
      MANIFEST

      eval_generate_resources_in(catalog, relationship_graph_for(catalog), 'Generator[thing]')

      expect(catalog).to have_resource('Notify[hello]')
    end

    it "should add a sentinel whit for the resource" do
      graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
        generator { thing:
          code => 'notify { hello: }'
        }
      MANIFEST

      find_vertex(graph, :whit, "completed_thing").must be_a(Puppet::Type.type(:whit))
    end

    it "should replace dependencies on the resource with dependencies on the sentinel" do
      graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
        generator { thing:
          code => 'notify { hello: }'
        }

        notify { last: require => Generator['thing'] }
      MANIFEST

      expect(graph).to enforce_order_with_edge(
        'Whit[completed_thing]', 'Notify[last]')
    end

    it "should add an edge from the nearest ancestor to the generated resource" do
      graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
        generator { thing:
          code => 'notify { hello: } notify { goodbye: }'
        }
      MANIFEST

      expect(graph).to enforce_order_with_edge(
        'Generator[thing]', 'Notify[hello]')
      expect(graph).to enforce_order_with_edge(
        'Generator[thing]', 'Notify[goodbye]')
    end

    it "should add an edge from each generated resource to the sentinel" do
      graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
        generator { thing:
          code => 'notify { hello: } notify { goodbye: }'
        }
      MANIFEST

      expect(graph).to enforce_order_with_edge(
        'Notify[hello]', 'Whit[completed_thing]')
      expect(graph).to enforce_order_with_edge(
        'Notify[goodbye]', 'Whit[completed_thing]')
    end

    it "should add an edge from the resource to the sentinel" do
      graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
        generator { thing:
          code => 'notify { hello: }'
        }
      MANIFEST

      expect(graph).to enforce_order_with_edge(
        'Generator[thing]', 'Whit[completed_thing]')
    end

    it "should contain the generated resources in the same container as the generator" do
      catalog = compile_to_ral(<<-MANIFEST)
        class container {
          generator { thing:
            code => 'notify { hello: }'
          }
        }

        include container
      MANIFEST

      eval_generate_resources_in(catalog, relationship_graph_for(catalog), 'Generator[thing]')

      expect(catalog).to contain_resources_equally('Generator[thing]', 'Notify[hello]')
    end

    it "should return false if an error occurred when generating resources" do
      catalog = compile_to_ral(<<-MANIFEST)
        generator { thing:
          code => 'fail("not a good generation")'
        }
      MANIFEST

      generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph_for(catalog), prioritizer)

      expect(generator.eval_generate(catalog.resource('Generator[thing]'))).
        to eq(false)
    end

    it "should return true if resources were generated" do
      catalog = compile_to_ral(<<-MANIFEST)
        generator { thing:
          code => 'notify { hello: }'
        }
      MANIFEST

      generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph_for(catalog), prioritizer)

      expect(generator.eval_generate(catalog.resource('Generator[thing]'))).
        to eq(true)
    end

    it "should not add a sentinel if no resources are generated" do
      catalog = compile_to_ral(<<-MANIFEST)
        generator { thing: }
      MANIFEST
      relationship_graph = relationship_graph_for(catalog)

      generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph, prioritizer)

      expect(generator.eval_generate(catalog.resource('Generator[thing]'))).
        to eq(false)
      expect(find_vertex(relationship_graph, :whit, "completed_thing")).to be_nil
    end

    it "orders generated resources with the generator" do
      graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
        notify { before: }
        generator { thing:
          code => 'notify { hello: }'
        }
        notify { after: }
      MANIFEST

      expect(order_resources_traversed_in(graph)).to(
        include_in_order("Notify[before]", "Generator[thing]", "Notify[hello]", "Notify[after]"))
    end

    it "orders the generator in manifest order with dependencies" do
      graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
        notify { before: }
        generator { thing:
          code => 'notify { hello: } notify { goodbye: }'
        }
        notify { third: require => Generator['thing'] }
        notify { after: }
      MANIFEST

      expect(order_resources_traversed_in(graph)).to(
        include_in_order("Notify[before]",
                         "Generator[thing]",
                         "Notify[hello]",
                         "Notify[goodbye]",
                         "Notify[third]",
                         "Notify[after]"))
    end

    it "duplicate generated resources are made dependent on the generator" do
      graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
        notify { before: }
        notify { hello: }
        generator { thing:
          code => 'notify { before: }'
        }
        notify { third: require => Generator['thing'] }
        notify { after: }
      MANIFEST

      expect(order_resources_traversed_in(graph)).to(
        include_in_order("Notify[hello]", "Generator[thing]", "Notify[before]", "Notify[third]", "Notify[after]"))
    end

    it "preserves dependencies on duplicate generated resources" do
      graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
        notify { before: }
        generator { thing:
          code => 'notify { hello: } notify { before: }',
          require => 'Notify[before]'
        }
        notify { third: require => Generator['thing'] }
        notify { after: }
      MANIFEST

      expect(order_resources_traversed_in(graph)).to(
        include_in_order("Notify[before]", "Generator[thing]", "Notify[hello]", "Notify[third]", "Notify[after]"))
    end

    def relationships_after_eval_generating(manifest, resource_to_generate)
      catalog = compile_to_ral(manifest)
      relationship_graph = relationship_graph_for(catalog)

      eval_generate_resources_in(catalog, relationship_graph, resource_to_generate)

      relationship_graph
    end

    def eval_generate_resources_in(catalog, relationship_graph, resource_to_generate)
      generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph, prioritizer)
      generator.eval_generate(catalog.resource(resource_to_generate))
    end
  end

  context "when applying generate" do
    it "should add the generated resources to the catalog" do
      catalog = compile_to_ral(<<-MANIFEST)
        generator { thing:
          kind => generate,
          code => 'notify { hello: }'
        }
      MANIFEST

      generate_resources_in(catalog, relationship_graph_for(catalog), 'Generator[thing]')

      expect(catalog).to have_resource('Notify[hello]')
    end

    it "should contain the generated resources in the same container as the generator" do
      catalog = compile_to_ral(<<-MANIFEST)
        class container {
          generator { thing:
            kind => generate,
            code => 'notify { hello: }'
          }
        }

        include container
      MANIFEST

      generate_resources_in(catalog, relationship_graph_for(catalog), 'Generator[thing]')

      expect(catalog).to contain_resources_equally('Generator[thing]', 'Notify[hello]')
    end

    it "should add an edge from the nearest ancestor to the generated resource" do
      graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
        generator { thing:
          kind => generate,
          code => 'notify { hello: } notify { goodbye: }'
        }
      MANIFEST

      expect(graph).to enforce_order_with_edge(
        'Generator[thing]', 'Notify[hello]')
      expect(graph).to enforce_order_with_edge(
        'Generator[thing]', 'Notify[goodbye]')
    end

    it "orders generated resources with the generator" do
      graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
        notify { before: }
        generator { thing:
          kind => generate,
          code => 'notify { hello: }'
        }
        notify { after: }
      MANIFEST

      expect(order_resources_traversed_in(graph)).to(
        include_in_order("Notify[before]", "Generator[thing]", "Notify[hello]", "Notify[after]"))
    end

    it "duplicate generated resources are made dependent on the generator" do
      graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
        notify { before: }
        notify { hello: }
        generator { thing:
          kind => generate,
          code => 'notify { before: }'
        }
        notify { third: require => Generator['thing'] }
        notify { after: }
      MANIFEST

      expect(order_resources_traversed_in(graph)).to(
        include_in_order("Notify[hello]", "Generator[thing]", "Notify[before]", "Notify[third]", "Notify[after]"))
    end

    it "preserves dependencies on duplicate generated resources" do
      graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
        notify { before: }
        generator { thing:
          kind => generate,
          code => 'notify { hello: } notify { before: }',
          require => 'Notify[before]'
        }
        notify { third: require => Generator['thing'] }
        notify { after: }
      MANIFEST

      expect(order_resources_traversed_in(graph)).to(
        include_in_order("Notify[before]", "Generator[thing]", "Notify[hello]", "Notify[third]", "Notify[after]"))
    end

    it "orders the generator in manifest order with dependencies" do
      graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
        notify { before: }
        generator { thing:
          kind => generate,
          code => 'notify { hello: } notify { goodbye: }'
        }
        notify { third: require => Generator['thing'] }
        notify { after: }
      MANIFEST

      expect(order_resources_traversed_in(graph)).to(
        include_in_order("Notify[before]",
                         "Generator[thing]",
                         "Notify[hello]",
                         "Notify[goodbye]",
                         "Notify[third]",
                         "Notify[after]"))
    end

    def relationships_after_generating(manifest, resource_to_generate)
      catalog = compile_to_ral(manifest)
      relationship_graph = relationship_graph_for(catalog)

      generate_resources_in(catalog, relationship_graph, resource_to_generate)

      relationship_graph
    end

    def generate_resources_in(catalog, relationship_graph, resource_to_generate)
      generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph, prioritizer)
      generator.generate_additional_resources(catalog.resource(resource_to_generate))
    end
  end

  def relationship_graph_for(catalog)
    relationship_graph = Puppet::Graph::RelationshipGraph.new(prioritizer)
    relationship_graph.populate_from(catalog)
    relationship_graph
  end

  def order_resources_traversed_in(relationships)
    order_seen = []
    relationships.traverse { |resource| order_seen << resource.ref }
    order_seen
  end

  RSpec::Matchers.define :contain_resources_equally do |*resource_refs|
    match do |catalog|
      @containers = resource_refs.collect do |resource_ref|
        catalog.container_of(catalog.resource(resource_ref)).ref
      end

      @containers.all? { |resource_ref| resource_ref == @containers[0] }
    end

    def failure_message_for_should
      "expected #{@expected.join(', ')} to all be contained in the same resource but the containment was #{@expected.zip(@containers).collect { |(res, container)| res + ' => ' + container }.join(', ')}"
    end
  end
end
