require 'spec_helper'
require 'matchers/json'
require 'puppet_spec/files'

describe Puppet::Node do
  include JSONMatchers
  include PuppetSpec::Files

  let(:environment) { Puppet::Node::Environment.create(:bar, []) }
  let(:env_loader) { Puppet::Environments::Static.new(environment) }

  describe "when managing its environment" do
    it "provides an environment instance" do
      expect(Puppet::Node.new("foo", :environment => environment).environment.name).to eq(:bar)
    end

    context "present in a loader" do
      around do |example|
        Puppet.override(:environments => env_loader) do
          example.run
        end
      end

      it "uses any set environment" do
        expect(Puppet::Node.new("foo", :environment => "bar").environment).to eq(environment)
      end

      it "determines its environment from its parameters if no environment is set" do
        expect(Puppet::Node.new("foo", :parameters => {"environment" => :bar}).environment).to eq(environment)
      end

      it "uses the configured environment if no environment is provided" do
        Puppet[:environment] = environment.name.to_s
        expect(Puppet::Node.new("foo").environment).to eq(environment)
      end

      it "allows the environment to be set after initialization" do
        node = Puppet::Node.new("foo")
        node.environment = :bar
        expect(node.environment.name).to eq(:bar)
      end

      it "sets environment_name with the correct environment name" do
        node = Puppet::Node.new("foo")
        node.environment = Puppet::Node::Environment.remote('www123')
        expect(node.environment_name).to eq(:www123)
      end

      it "allows its environment to be set by parameters after initialization" do
        node = Puppet::Node.new("foo")
        node.parameters["environment"] = :bar
        expect(node.environment.name).to eq(:bar)
      end
    end
  end

  describe "serialization" do
    around do |example|
      Puppet.override(:environments => env_loader) do
        example.run
      end
    end

    it "can round-trip through json" do
      Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b")
      node = Puppet::Node.new("hello",
                              :environment => 'bar',
                              :classes => ['erth', 'aiu'],
                              :parameters => {"hostname"=>"food"}
                             )
      new_node = Puppet::Node.convert_from('json', node.render('json'))
      expect(new_node.environment).to eq(node.environment)
      expect(new_node.parameters).to eq(node.parameters)
      expect(new_node.classes).to eq(node.classes)
      expect(new_node.name).to eq(node.name)
    end

    it "validates against the node json schema" do
      Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b")
      node = Puppet::Node.new("hello",
                              :environment => 'bar',
                              :classes => ['erth', 'aiu'],
                              :parameters => {"hostname"=>"food"}
                             )
      expect(node.to_json).to validate_against('api/schemas/node.json')
    end

    it "when missing optional parameters validates against the node json schema" do
      Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b")
      node = Puppet::Node.new("hello",
                              :environment => 'bar'
                             )
      expect(node.to_json).to validate_against('api/schemas/node.json')
    end

    it "should allow its environment parameter to be set by attribute after initialization" do
      node = Puppet::Node.new("foo", { :parameters => { 'environment' => :foo } })
      node.environment_name = :foo
      node.environment = :bar
      expect(node.environment_name).to eq(:bar)
      expect(node.parameters['environment']).to eq('bar')
    end

    it 'to_data_hash returns value that is instance of to Data' do
      node = Puppet::Node.new("hello",
        :environment => 'bar',
        :classes => ['erth', 'aiu'],
        :parameters => {"hostname"=>"food"}
      )
      expect(Puppet::Pops::Types::TypeFactory.data.instance?(node.to_data_hash)).to be_truthy
    end
  end

  describe "when serializing using yaml" do
    before do
      @node = Puppet::Node.new("mynode")
    end

    it "a node can roundtrip" do
      expect(Puppet::Util::Yaml.safe_load(@node.to_yaml, [Puppet::Node]).name).to eql("mynode")
    end

    it "limits the serialization of environment to be just the name" do
      yaml_file = file_containing("temp_yaml", @node.to_yaml)
      expect(File.read(yaml_file)).to eq(<<~NODE)
        --- !ruby/object:Puppet::Node
        name: mynode
        environment: production
      NODE
    end
  end

  describe "when serializing using yaml and values classes and parameters are missing in deserialized hash" do
    it "a node can roundtrip" do
      @node = Puppet::Node.from_data_hash({'name' => "mynode"})
      expect(Puppet::Util::Yaml.safe_load(@node.to_yaml, [Puppet::Node]).name).to eql("mynode")
    end

    it "errors if name is nil" do
      expect { Puppet::Node.from_data_hash({ })}.to raise_error(ArgumentError, /No name provided in serialized data/)
    end
  end

  describe "when converting to json" do
    before do
      @node = Puppet::Node.new("mynode")
    end

    it "provide its name" do
      expect(@node).to set_json_attribute('name').to("mynode")
    end

    it "includes the classes if set" do
      @node.classes = %w{a b c}
      expect(@node).to set_json_attribute("classes").to(%w{a b c})
    end

    it "does not include the classes if there are none" do
      expect(@node).to_not set_json_attribute('classes')
    end

    it "includes parameters if set" do
      @node.parameters = {"a" => "b", "c" => "d"}
      expect(@node).to set_json_attribute('parameters').to({"a" => "b", "c" => "d"})
    end

    it "does not include the environment parameter in the json" do
      @node.parameters = {"a" => "b", "c" => "d"}
      @node.environment = environment
      expect(@node.parameters).to eq({"a"=>"b", "c"=>"d", "environment"=>"bar"})
      expect(@node).to set_json_attribute('parameters').to({"a" => "b", "c" => "d"})
    end

    it "does not include the parameters if there are none" do
      expect(@node).to_not set_json_attribute('parameters')
    end

    it "includes the environment" do
      @node.environment = "production"
      expect(@node).to set_json_attribute('environment').to('production')
    end
  end

  describe "when converting from json" do
    before do
      @node = Puppet::Node.new("mynode")
      @format = Puppet::Network::FormatHandler.format('json')
    end

    def from_json(json)
      @format.intern(Puppet::Node, json)
    end

    it "sets its name" do
      expect(Puppet::Node).to read_json_attribute('name').from(@node.to_json).as("mynode")
    end

    it "includes the classes if set" do
      @node.classes = %w{a b c}
      expect(Puppet::Node).to read_json_attribute('classes').from(@node.to_json).as(%w{a b c})
    end

    it "includes parameters if set" do
      @node.parameters = {"a" => "b", "c" => "d"}
      expect(Puppet::Node).to read_json_attribute('parameters').from(@node.to_json).as({"a" => "b", "c" => "d", "environment" => "production"})
    end

    it "deserializes environment to environment_name as a symbol" do
      Puppet.override(:environments => env_loader) do
        @node.environment = environment
        expect(Puppet::Node).to read_json_attribute('environment_name').from(@node.to_json).as(:bar)
      end
    end

    it "does not immediately populate the environment instance" do
      node = described_class.from_data_hash("name" => "foo", "environment" => "production")

      expect(node.environment_name).to eq(:production)
      expect(node).not_to be_has_environment_instance
      node.environment
      expect(node).to be_has_environment_instance
    end
  end
end

describe Puppet::Node, "when initializing" do
  before do
    @node = Puppet::Node.new("testnode")
  end

  it "sets the node name" do
    expect(@node.name).to eq("testnode")
  end

  it "does not allow nil node names" do
    expect { Puppet::Node.new(nil) }.to raise_error(ArgumentError)
  end

  it "defaults to an empty parameter hash" do
    expect(@node.parameters).to eq({})
  end

  it "defaults to an empty class array" do
    expect(@node.classes).to eq([])
  end

  it "notes its creation time" do
    expect(@node.time).to be_instance_of(Time)
  end

  it "accepts parameters passed in during initialization" do
    params = {"a" => "b"}
    @node = Puppet::Node.new("testing", :parameters => params)
    expect(@node.parameters).to eq(params)
  end

  it "accepts classes passed in during initialization" do
    classes = %w{one two}
    @node = Puppet::Node.new("testing", :classes => classes)
    expect(@node.classes).to eq(classes)
  end

  it "always returns classes as an array" do
    @node = Puppet::Node.new("testing", :classes => "myclass")
    expect(@node.classes).to eq(["myclass"])
  end
end

describe Puppet::Node, "when merging facts" do
  before do
    @node = Puppet::Node.new("testnode")
    Puppet[:facts_terminus] = :memory
    Puppet::Node::Facts.indirection.save(Puppet::Node::Facts.new(@node.name, "one" => "c", "two" => "b"))
  end

  context "when supplied facts as a parameter" do
    let(:facts) { Puppet::Node::Facts.new(@node.name, "foo" => "bar") }

    it "accepts facts to merge with the node" do
      expect(@node).to receive(:merge).with({ 'foo' => 'bar' })
      @node.fact_merge(facts)
    end

    it "will not query the facts indirection if facts are supplied" do
      expect(Puppet::Node::Facts.indirection).not_to receive(:find)
      @node.fact_merge(facts)
    end
  end

  it "recovers with a Puppet::Error if something is thrown from the facts indirection" do
    expect(Puppet::Node::Facts.indirection).to receive(:find).and_raise("something bad happened in the indirector")
    expect { @node.fact_merge }.to raise_error(Puppet::Error, /Could not retrieve facts for testnode: something bad happened in the indirector/)
  end

  it "prefers parameters already set on the node over facts from the node" do
    @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"})
    @node.fact_merge
    expect(@node.parameters["one"]).to eq("a")
  end

  it "adds passed parameters to the parameter list" do
    @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"})
    @node.fact_merge
    expect(@node.parameters["two"]).to eq("b")
  end

  it "warns when a parameter value is not updated" do
    @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"})
    expect(Puppet).to receive(:warning).with('The node parameter \'one\' for node \'testnode\' was already set to \'a\'. It could not be set to \'b\'')
    @node.merge "one" => "b"
  end

  it "accepts arbitrary parameters to merge into its parameters" do
    @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"})
    @node.merge "two" => "three"
    expect(@node.parameters["two"]).to eq("three")
  end

  context "with an env loader" do
    let(:environment) { Puppet::Node::Environment.create(:one, []) }
    let(:environment_two) { Puppet::Node::Environment.create(:two, []) }
    let(:environment_three) { Puppet::Node::Environment.create(:three, []) }
    let(:environment_prod) { Puppet::Node::Environment.create(:production, []) }
    let(:env_loader) { Puppet::Environments::Static.new(environment, environment_two, environment_three, environment_prod) }

    around do |example|
      Puppet.override(:environments => env_loader) do
        example.run
      end
    end

    context "when a node is initialized from a data hash" do
      context "when a node is initialzed with an environment" do
        it "uses 'environment' when provided" do
          my_node = Puppet::Node.from_data_hash("environment" => "one", "name" => "my_node")
          expect(my_node.environment.name).to eq(:one)
        end

        it "uses the environment parameter when provided" do
          my_node = Puppet::Node.from_data_hash("parameters" => {"environment" => "one"}, "name" => "my_node")
          expect(my_node.environment.name).to eq(:one)
        end

        it "uses the environment when also given an environment parameter" do
          my_node = Puppet::Node.from_data_hash("parameters" => {"environment" => "one"}, "name" => "my_node", "environment" => "two")
          expect(my_node.environment.name).to eq(:two)
        end

        it "uses 'environment' when an environment fact has been merged" do
          my_node = Puppet::Node.from_data_hash("environment" => "one", "name" => "my_node")
          my_node.fact_merge Puppet::Node::Facts.new "my_node", "environment" => "two"
          expect(my_node.environment.name).to eq(:one)
        end

        it "uses an environment parameter when an environment fact has been merged" do
          my_node = Puppet::Node.from_data_hash("parameters" => {"environment" => "one"}, "name" => "my_node")
          my_node.fact_merge Puppet::Node::Facts.new "my_node", "environment" => "two"
          expect(my_node.environment.name).to eq(:one)
        end

        it "uses an environment when an environment parameter has been given and an environment fact has been merged" do
          my_node = Puppet::Node.from_data_hash("parameters" => {"environment" => "two"}, "name" => "my_node", "environment" => "one")
          my_node.fact_merge Puppet::Node::Facts.new "my_node", "environment" => "three"
          expect(my_node.environment.name).to eq(:one)
        end
      end

      context "when a node is initialized without an environment" do
        it "should use the server's default environment" do
          my_node = Puppet::Node.from_data_hash("name" => "my_node")
          expect(my_node.environment.name).to eq(:production)
        end

        it "should use the server's default when an environment fact has been merged" do
          my_node = Puppet::Node.from_data_hash("name" => "my_node")
          my_node.fact_merge Puppet::Node::Facts.new "my_node", "environment" => "two"
          expect(my_node.environment.name).to eq(:production)
        end
      end
    end

    context "when a node is initialized from new" do
      context "when a node is initialzed with an environment" do
        it "adds the environment to the list of parameters" do
          Puppet[:environment] = "one"
          @node = Puppet::Node.new("testnode", :environment => "one")
          @node.merge "two" => "three"
          expect(@node.parameters["environment"]).to eq("one")
        end

        it "when merging, syncs the environment parameter to a node's existing environment" do
          @node = Puppet::Node.new("testnode", :environment => "one")
          @node.merge "environment" => "two"
          expect(@node.parameters["environment"]).to eq("one")
        end

        it "merging facts does not override that environment" do
          @node = Puppet::Node.new("testnode", :environment => "one")
          @node.fact_merge Puppet::Node::Facts.new "testnode", "environment" => "two"
          expect(@node.environment.name.to_s).to eq("one")
        end
      end

      context "when a node is initialized without an environment" do
        it "it perfers an environment name to an environment fact" do
          @node = Puppet::Node.new("testnode")
          @node.environment_name = "one"
          @node.fact_merge Puppet::Node::Facts.new "testnode", "environment" => "two"
          expect(@node.environment.name.to_s).to eq("one")
        end
      end
    end
  end
end

describe Puppet::Node, "when indirecting" do
  it "defaults to the 'plain' node terminus" do
    Puppet::Node.indirection.reset_terminus_class

    expect(Puppet::Node.indirection.terminus_class).to eq(:plain)
  end
end

describe Puppet::Node, "when generating the list of names to search through" do
  before do
    @node = Puppet::Node.new("foo.domain.com",
                             :parameters => {"hostname" => "yay", "domain" => "domain.com"})
  end

  it "returns an array of one name" do
    expect(@node.names).to be_instance_of(Array)
    expect(@node.names).to eq ["foo.domain.com"]
  end
end
