#!/usr/bin/env rspec -cfd

require_relative '../spec_helper'

require 'time'
require 'arborist/node'


describe Arborist::Node do

	before( :all ) do
		Arborist::Event.load_all
	end
	before( :each ) do
		@real_derivatives = described_class.derivatives.dup
	end
	after( :each ) do
		described_class.derivatives.replace( @real_derivatives )
	end


	let( :concrete_class ) do
		Class.new( described_class )
	end

	let( :identifier ) { 'the_identifier' }
	let( :identifier2 ) { 'the_other_identifier' }


	### Suppress $VERBOSE warnings for the duration of a block
	def with_warnings_suppressed
		oldwarn = $VERBOSE
		$VERBOSE = nil
		yield
	ensure
		$VERBOSE = oldwarn
	end


	shared_examples_for "a reachable node" do

		it "is still 'reachable'" do
			expect( node ).to be_reachable
			expect( node ).to_not be_unreachable
		end

	end


	shared_examples_for "an unreachable node" do

		it "is not 'reachable'" do
			expect( node ).to_not be_reachable
			expect( node ).to be_unreachable
		end

	end


	it "can be loaded from a file" do
		concrete_instance = nil
		expect( Kernel ).to receive( :load ).with( "a/path/to/a/node.rb" ) do
			concrete_instance = concrete_class.new( identifier )
		end

		result = described_class.load( "a/path/to/a/node.rb" )
		expect( result ).to be_an( Array )
		expect( result.length ).to eq( 1 )
		expect( result ).to include( concrete_instance )
	end


	it "can be constructed from a Hash" do
		instance = concrete_class.new( identifier,
			parent: 'branch',
			description: 'A testing node',
			tags: ['internal', 'testing']
		)

		expect( instance ).to be_a( described_class )
		expect( instance.parent ).to eq( 'branch' )
		expect( instance.description ).to eq( 'A testing node' )
		expect( instance.tags ).to include( 'internal', 'testing' )
	end


	it "can load multiple nodes from a single file" do
		concrete_instance1 = concrete_instance2 = nil
		expect( Kernel ).to receive( :load ).with( "a/path/to/a/node.rb" ) do
			concrete_instance1 = concrete_class.new( identifier )
			concrete_instance2 = concrete_class.new( identifier2 )
		end

		result = described_class.load( "a/path/to/a/node.rb" )
		expect( result ).to be_an( Array )
		expect( result.length ).to eq( 2 )
		expect( result ).to include( concrete_instance1, concrete_instance2 )
	end


	it "knows what its identifier is" do
		expect( described_class.new('good_identifier').identifier ).to eq( 'good_identifier' )
	end


	it "accepts identifiers with hyphens" do
		expect( described_class.new('router_nat-pmp').identifier ).to eq( 'router_nat-pmp' )
	end


	it "raises an error if the node identifier is invalid" do
		expect {
		   described_class.new 'bad identifier'
		}.to raise_error( RuntimeError, /identifier/i )
	end


	context "subnode classes" do

		it "can declare the type of node they live under" do
			subnode_class = Class.new( described_class )
			subnode_class.parent_type( concrete_class )

			expect( subnode_class.parent_types ).to include( concrete_class )
		end

		it "can be constructed via a factory method on instances of their parent type" do
			subnode_class = with_warnings_suppressed do
				Class.new( described_class ) do
					def self::name; "TestSubNode"; end
					def self::plugin_name; "testsub"; end
				end
			end
			described_class.derivatives['testsub'] = subnode_class

			subnode_class.parent_type( concrete_class )
			parent = concrete_class.new( 'branch' )
			node = parent.testsub( 'leaf' )

			expect( node ).to be_an_instance_of( subnode_class )
			expect( node.identifier ).to eq( 'leaf' )
			expect( node.parent ).to eq( 'branch' )
		end


		it "can pre-process the factory method arguments" do
			subnode_class = with_warnings_suppressed do
				Class.new( described_class ) do
					def self::name; "TestSubNode"; end
					def self::plugin_name; "testsub"; end
					def args( new_args=nil )
						@args = new_args if new_args
						return @args
					end
					def modify( attributes )
						attributes = stringify_keys( attributes )
						super
						self.args( attributes['args'] )
					end
				end
			end
			described_class.derivatives['testsub'] = subnode_class

			subnode_class.parent_type( concrete_class ) do |arg1, id, *args|
				[ id, {args: [arg1] + args} ]
			end

			parent = concrete_class.new( 'branch' )
			node = parent.testsub( :arg1, 'leaf', :arg2, :arg3 )

			expect( node ).to be_an_instance_of( subnode_class )
			expect( node.parent ).to eq( parent.identifier )
			expect( node.args ).to eq([ :arg1, :arg2, :arg3 ])
		end

	end


	context "an instance of a concrete subclass" do

		let( :parent_node ) { concrete_class.new(identifier) }
		let( :sibling_node ) do
			concrete_class.new( 'sibling' ) do
				parent 'the_identifier'
			end
		end
		let( :node ) do
			concrete_class.new( identifier2 ) do
				parent 'the_identifier'
			end
		end


		it "can declare what its parent is by identifier" do
			expect( node.parent ).to eq( identifier )
		end


		it "can have child nodes added to it" do
			parent_node.add_child( node )
			expect( parent_node.children ).to include( node.identifier )
		end


		it "can have child nodes appended to it" do
			parent_node << node
			expect( parent_node.children ).to include( node.identifier )
		end

		it "knows its family catagorization" do
			expect( parent_node.family ).to eq( :node )
		end

		it "raises an error if a node which specifies a different parent is added to it" do
			stranger_node = concrete_class.new( identifier2 ) do
				parent 'youre_not_my_mother'
			end
			expect {
				parent_node.add_child( stranger_node )
			}.to raise_error( /not a child of/i )
		end


		it "doesn't add the same child more than once" do
			parent_node.add_child( node )
			parent_node.add_child( node )
			expect( parent_node.children.size ).to eq( 1 )
		end


		it "knows it doesn't have any children if it's empty" do
			expect( parent_node ).to_not have_children
		end


		it "knows it has children if subnodes have been added" do
			parent_node.add_child( node )
			expect( parent_node ).to have_children
		end


		it "knows how to remove one of its children" do
			parent_node.add_child( node )
			parent_node.remove_child( node )
			expect( parent_node ).to_not have_children
		end


		it "starts out in `unknown` status" do
			expect( parent_node ).to be_unknown
		end


		it "remembers status time changes" do
			expect( node.status_changed ).to eq( Time.at(0) )

			time = Time.at( 1523900910 )
			allow( Time ).to receive( :now ).and_return( time )

			node.update( { error: 'boom' } )
			expect( node ).to be_down
			expect( node.status_changed ).to eq( time )
			expect( node.status_last_changed ).to eq( Time.at(0) )


			node.update( {} )
			expect( node ).to be_up
			expect( node.status_last_changed ).to eq( time )
		end


		it "stores a history of its status" do
			node.status_history_size( 3 )

			node.update( {} )
			node.update( {} )
			node.update( { warning: 'whoopsie' } )
			node.update( {} )

			expect( node.status_history ).to eq( ['up', 'warn', 'up'] )
		end


		it "knows if its status is transitioning frequently" do
			node.status_history_size( 10 )
			node.flap_threshold( 3 )

			7.times{ node.update( {} ) }
			node.update( { error: 'boooM!' } )
			node.update( { warning: 'whoopsie' } )
			node.update( {} )

			expect( node ).to be_flapping

			node.flap_threshold( 4 )
			node.update( {} )

			expect( node ).to_not be_flapping
		end


		it "groups errors from separate monitors by their key" do
			expect( node ).to be_unknown

			node.update( {error: 'ded'}, 'MonitorTron2000' )
			node.update( {error: 'moar ded'}, 'MonitorTron5000' )
			expect( node ).to be_down

			expect( node.errors.length ).to eq( 2 )
			node.update( {}, 'MonitorTron5000' )

			expect( node ).to be_down
			expect( node.errors.length ).to eq( 1 )

			node.update( {}, 'MonitorTron2000' )
			expect( node ).to be_up
		end


		it "sets a default monitor key" do
			node.update( error: 'ded' )
			expect( node ).to be_down
			expect( node.errors ).to eq({ '_' => 'ded' })
		end


		describe "in `unknown` status" do

			let( :node ) do
				obj = super()
				obj.status = 'unknown'
				obj
			end


			it_behaves_like "a reachable node"


			it "transitions to `up` status if doesn't have any errors after an update" do
				expect {
					node.update( tested: true )
				}.to change { node.status }.from( 'unknown' ).to( 'up' )
			end


			it "transitions to `down` status if its state is updated with an `error` property" do
				expect {
					node.update( error: "Couldn't talk to it!" )
				}.to change { node.status }.from( 'unknown' ).to( 'down' )
			end


			it "transitions to `warn` status if its state is updated with a `warning` property" do
				expect {
					node.update( warning: "Things are starting to look bad!" )
				}.to change { node.status }.from( 'unknown' ).to( 'warn' )
			end


			it "transitions to `disabled` if it's acknowledged" do
				expect {
					node.acknowledge( message: "Maintenance", sender: 'mahlon' )
				}.to change { node.status }.from( 'unknown' ).to( 'disabled' )
			end

		end


		describe "in `up` status" do

			let( :node ) do
				obj = super()
				obj.status = 'up'
				obj
			end


			it_behaves_like "a reachable node"


			it "stays in `up` status if doesn't have any errors after an update" do
				expect {
					node.update( tested: true )
				}.to_not change { node.status }.from( 'up' )
			end


			it "transitions to `down` status if its state is updated with an `error` property" do
				expect {
					node.update( error: "Couldn't talk to it!" )
				}.to change { node.status }.from( 'up' ).to( 'down' )
			end


			it "transitions to `down` status if it's updated with both an `error` and `warning` property" do
				expect {
					node.update( error: "Couldn't talk to it!", warning: "Above configured levels!" )
				}.to change { node.status }.from( 'up' ).to( 'down' )
			end


			it "transitions to `warn` status if its state is updated with a `warning` property" do
				expect {
					node.update( warning: "Things are starting to look bad!" )
				}.to change { node.status }.from( 'up' ).to( 'warn' )
			end


			it "transitions to `disabled` if it's acknowledged" do
				expect {
					node.acknowledge( message: "Maintenance", sender: 'mahlon' )
				}.to change { node.status }.from( 'up' ).to( 'disabled' )
			end


			it "transitions to `quieted` if it's notified that its parent has gone down" do
				down_event = Arborist::Event.create( :node_down, parent_node )
				expect {
					node.handle_event( down_event )
				}.to change { node.status }.from( 'up' ).to( 'quieted' )
			end


			it "records its transition to quieted in its status history" do
				node.status_history_size( 3 )
				down_event = Arborist::Event.create( :node_down, parent_node )

				expect( node.status ).to eq( 'up' )
				node.handle_event( down_event )
				expect( node.status ).to eq( 'quieted' )

				expect( node.status_history ).to eq( ['quieted'] )
			end
		end


		describe "in `down` status" do

			let( :node ) do
				obj = super()
				obj.status = 'down'
				obj.errors['moldovia'] = 'Something is wrong | he falls | betraying the trust | "\
					"there is a disaster in his life.'
				obj
			end


			it_behaves_like "an unreachable node"


			it "transitions to `acked` status if it's acknowledged" do
				expect {
					node.acknowledge( message: "Leitmotiv", sender: 'ged' )
				}.to change { node.status }.from( 'down' ).to( 'acked' )
			end


			it "transitions to `up` status if all of its errors are cleared" do
				expect {
					node.update( {error: nil}, 'moldovia' )
				}.to change { node.status }.from( 'down' ).to( 'up' )
			end


			it "transitions to `warn` status if errors are cleared but warnings remain" do
				expect {
					node.update( {error: nil, warning: 'squirt!'}, 'moldovia' )
				}.to change { node.status }.from( 'down' ).to( 'warn' )
			end

		end


		describe "in `warn` status" do

			let( :node ) do
				obj = super()
				obj.status = 'warn'
				obj.warnings = { 'beach' => 'Sweaty but functional servers.' }
				obj
			end


			it_behaves_like "a reachable node"


			it "transitions to `up` if its warnings are cleared" do
				expect {
					node.update( {warning: nil}, 'beach' )
				}.to change { node.status }.from( 'warn' ).to( 'up' )
			end


			it "transitions to `down` if has an error set" do
				expect {
					node.update( {error: "Shark warning."}, 'beach' )
				}.to change { node.status }.from( 'warn' ).to( 'down' )
			end


			it "transitions to `disabled` if it's acknowledged" do
				expect {
					node.acknowledge( message: "Chill", sender: 'ged' )
				}.to change { node.status }.from( 'warn' ).to( 'disabled' )
			end

		end


		describe "in `acked` status" do

			let( :node ) do
				obj = super()
				obj.status = 'down'
				obj.errors['moldovia'] = 'Something is wrong | he falls | betraying the trust | "\
					"there is a disaster in his life.'
				obj.acknowledge( message: "Leitmotiv", sender: 'ged' )  # ack!
				obj
			end


			it_behaves_like "a reachable node"

			it "transitions to `up` status if its error is cleared" do
				expect {
					node.update( {error: nil}, 'moldovia' )
				}.to change { node.status }.from( 'acked' ).to( 'up' )
			end


			it "transitions to `disabled` status if its acked twice" do
				expect {
					node.acknowledge( message: "Leitmotiv", sender: 'ged' )
				}.to change { node.status }.from( 'acked' ).to( 'disabled' )
				expect( node.errors ).to be_empty
			end


			it "stays `up` if it is updated twice with an error key" do
				node.update( {error: nil}, 'moldovia' )

				expect {
					node.update( {error: nil}, 'moldovia' ) # make sure it stays cleared
				}.to_not change { node.status }.from( 'up' )
			end

		end


		describe "in `disabled` status" do

			let( :node ) do
				obj = super()
				obj.acknowledge( message: "Bikini models", sender: 'ged' )
				obj
			end


			it_behaves_like "an unreachable node"


			it "stays `disabled` if it gets an error" do
				expect {
					node.update( error: "take me to the virus hospital" )
				}.to_not change { node.status }.from( 'disabled' )

				expect( node.ack ).to_not be_nil
			end


			it "stays `disabled` if it gets a warning" do
				expect {
					node.update( warning: "heartbone" )
				}.to_not change { node.status }.from( 'disabled' )

				expect( node.ack ).to_not be_nil
			end


			it "stays `disabled` if it gets a successful update" do
				expect {
					node.update( ping: {time: 0.02} )
				}.to_not change { node.status }.from( 'disabled' )

				expect( node.ack ).to_not be_nil
			end


			it "transitions to `unknown` if its acknowledgment is cleared" do
				expect {
					node.unacknowledge
				}.to change { node.status }.from( 'disabled' ).to( 'unknown' )

				expect( node.ack ).to be_nil
			end

		end


		describe "in `quieted` status because its parent is down" do

			let( :down_event ) { Arborist::Event.create(:node_down, parent_node) }
			let( :up_event ) { Arborist::Event.create(:node_up, parent_node) }

			let( :node ) do
				obj = super()
				obj.handle_event( down_event )
				obj
			end


			it_behaves_like "an unreachable node"


			it "remains `quieted` even if updated with an error" do
				expect {
					node.update( {error: "Internal error"}, 'webservice' )
				}.to_not change { node.status }.from( 'quieted' )
			end


			it "transitions to `unknown` if its parent transitions to up" do
				up_event = Arborist::Event.create( :node_up, parent_node )

				expect {
					node.handle_event( up_event )
				}.to change { node.status }.from( 'quieted' ).to( 'unknown' )
			end


			it "transitions to `unknown` if its parent transitions to warn" do
				warn_event = Arborist::Event.create( :node_warn, parent_node )

				expect {
					node.handle_event( warn_event )
				}.to change { node.status }.from( 'quieted' ).to( 'unknown' )
			end


			it "transitions to `disabled` if it's acknowledged" do
				expect {
					node.acknowledge( message: 'Turning this off for now.', sender: 'ged' )
				}.to change { node.status }.from( 'quieted' ).to( 'disabled' )
			end

		end


		describe "in `quieted` status because one of its dependencies is down" do

			let( :down_event ) { Arborist::Event.create(:node_down, sibling_node) }
			let( :up_event ) { Arborist::Event.create(:node_up, sibling_node) }

			let( :node ) do
				obj = super()
				obj.depends_on( 'sibling' )
				obj.handle_event( down_event )
				obj
			end


			it_behaves_like "an unreachable node"


			it "transitions to `unknown` if its reasons for being quieted are cleared" do
				expect {
					node.handle_event( up_event )
				}.to change { node.status }.from( 'quieted' ).to( 'unknown' )
			end


			it "transitions to `disabled` if it's acknowledged" do
				expect {
					node.acknowledge( message: 'Turning this off for now.', sender: 'ged' )
				}.to change { node.status }.from( 'quieted' ).to( 'disabled' )
			end

		end


		describe "Properties API" do

			it "is initialized with an empty set" do
				expect( node.properties ).to be_empty
			end

			it "can attach arbitrary values to the node" do
				node.update( 'cider' => 'tasty' )
				expect( node.properties['cider'] ).to eq( 'tasty' )
			end

			it "replaces existing values on update" do
				node.properties.replace({
					'cider' => 'tasty',
					'cider_size' => '16oz',
				})
				node.update( 'cider_size' => '8oz' )

				expect( node.properties ).to include(
					'cider' => 'tasty',
					'cider_size' => '8oz'
				)
			end

			it "replaces nested values on update" do
				node.properties.replace({
					'cider' => {
						'description' => 'tasty',
						'size' => '16oz',
					},
					'sausage' => {
						'description' => 'pork',
						'size' => 'huge',
					},
					'music' => '80s'
				})
				node.update(
					'cider' => {'size' => '8oz'},
					'sausage' => 'Linguiça',
					'music' => {
						'genre' => '80s',
						'artist' => 'The Smiths'
					}
				)

				expect( node.properties ).to eq(
					'cider' => {
						'description' => 'tasty',
						'size' => '8oz',
					},
					'sausage' => 'Linguiça',
					'music' => {
						'genre' => '80s',
						'artist' => 'The Smiths'
					}
				)
			end

			it "removes pairs whose value is nil" do
				node.properties.replace({
					'cider' => {
						'description' => 'tasty',
						'size' => '16oz',
					},
					'sausage' => {
						'description' => 'pork',
						'size' => 'huge',
					},
					'music' => '80s'
				})
				node.update(
					'cider' => {'size' => nil},
					'sausage' => nil,
					'music' => {
						'genre' => '80s',
						'artist' => 'The Smiths'
					}
				)

				expect( node.properties ).to eq(
					'cider' => {
						'description' => 'tasty',
					},
					'music' => {
						'genre' => '80s',
						'artist' => 'The Smiths'
					}
				)
			end
		end


		describe "Enumeration" do

			it "iterates over its children for #each" do
				parent = parent_node
				parent <<
					concrete_class.new('child1') { parent 'the_identifier' } <<
					concrete_class.new('child2') { parent 'the_identifier' } <<
					concrete_class.new('child3') { parent 'the_identifier' }

				expect( parent_node.map(&:identifier) ).to eq([ 'child1', 'child2', 'child3' ])
			end

		end


		describe "Serialization" do

			# From spec_helper.rb
			let( :concrete_class ) { TestNode }
			let( :node ) do
				concrete_class.new( 'foo' ) do
					parent 'bar'
					description "The prototypical node"
					tags :chunker, :hunky, :flippin, :hippo

					depends_on(
						all_of('postgres', 'rabbitmq', 'memcached', on: 'svchost'),
						any_of('webproxy', on: ['fe-host1','fe-host2','fe-host3'])
					)

					config os: 'freebsd-10'

					update( 'song' => 'Around the World', 'artist' => 'Daft Punk', 'length' => '7:09' )
				end
			end

			let( :tree ) do
				node_hierarchy( node,
					node_hierarchy( 'host-a',
						testing_node( 'host-a-www' ),
						testing_node( 'host-a-smtp' ),
						testing_node( 'host-a-imap' )
					),
					node_hierarchy( 'host-b',
						testing_node( 'host-b-www' ),
						testing_node( 'host-b-nfs' ),
						testing_node( 'host-b-ssh' )
					),
					node_hierarchy( 'host-c',
						testing_node( 'host-c-www' )
					),
					node_hierarchy( 'host-d',
						testing_node( 'host-d-ssh' ),
						testing_node( 'host-d-amqp' ),
						testing_node( 'host-d-database' ),
						testing_node( 'host-d-memcached' )
					)
				)
			end


			it "can restore saved state from an older copy of the node" do
				old_node = Marshal.load( Marshal.dump(node) )

				old_node.status = 'down'
				old_node.status_changed = Time.now - 400
				old_node.status_last_changed = Time.now - 800
				old_node.errors = "Host unreachable"
				old_node.update(
					ack: {
						'time' => Time.now - 200,
						'message' => "Technician dispatched.",
						'sender' => 'darby@example.com'
					}
				)
				old_node.properties.replace(
					'ping' => {
						'ttl' => 0.23
					}
				)
				old_node.last_contacted = Time.now - 28
				old_node.dependencies.mark_down( 'svchost-postgres' )
				old_node.status_history << 'up'
				old_node.flapping = true

				node.restore( old_node )

				expect( node.status ).to eq( old_node.status )
				expect( node.status_changed ).to eq( old_node.status_changed )
				expect( node.status_last_changed ).to eq( old_node.status_last_changed )
				expect( node.status_history ).to eq( old_node.status_history )
				expect( node.flapping? ).to eq( old_node.flapping? )
				expect( node.errors ).to eq( old_node.errors )
				expect( node.ack ).to eq( old_node.ack )
				expect( node.properties ).to include( old_node.properties )
				expect( node.last_contacted ).to eq( old_node.last_contacted )
				expect( node.dependencies ).to eql( old_node.dependencies )
			end


			it "doesn't restore operational attributes from the node file on disk with those from saved state" do
				old_node = Marshal.load( Marshal.dump(node) )
				node_copy = Marshal.load( Marshal.dump(node) )

				old_node.instance_variable_set( :@parent, 'foo' )
				old_node.instance_variable_set( :@description, 'Some older description' )
				old_node.instance_variable_set( :@config, {'os' => 'freebsd-8'} )
				old_node.tags( :bunker, :lucky, :tickle, :trucker )
				old_node.source = '/somewhere/else'

				node.restore( old_node )

				expect( node.parent ).to eq( node_copy.parent )
				expect( node.description ).to eq( node_copy.description )
				expect( node.tags ).to eq( node_copy.tags )
				expect( node.source ).to eq( node_copy.source )
				expect( node.dependencies ).to eq( node_copy.dependencies )
				expect( node.config ).to eq( node_copy.config )
			end


			it "doesn't replace dependencies if they've changed" do
				old_node = Marshal.load( Marshal.dump(node) )
				old_node.dependencies.mark_down( 'svchost-postgres' )
				old_node.dependencies.mark_down( 'svchost-rabbitmq' )

				# Drop 'svchost-rabbitmq'
				node.depends_on(
					node.all_of('postgres', 'memcached', on: 'svchost'),
					node.any_of('webproxy', on: ['fe-host1','fe-host2','fe-host3'])
				)

				node.restore( old_node )

				expect( node.dependencies ).to_not eql( old_node.dependencies )
				expect( node.dependencies.all_identifiers ).to_not include( 'svchost-rabbitmq' )
				expect( node.dependencies.down_subdeps.length ).to eq( 1 )
			end


			it "can return a Hash of serializable node data" do
				result = tree.to_h

				expect( result ).to be_a( Hash )
				expect( result ).to include(
					:identifier,
					:parent, :description, :tags, :properties, :ack, :status,
					:last_contacted, :status_changed, :errors, :quieted_reasons,
					:dependencies, :status_last_changed
				)
				expect( result[:identifier] ).to eq( 'foo' )
				expect( result[:type] ).to eq( 'testnode' )
				expect( result[:parent] ).to eq( 'bar' )
				expect( result[:description] ).to eq( node.description )
				expect( result[:tags] ).to eq( node.tags )
				expect( result[:properties] ).to eq( node.properties )
				expect( result[:ack] ).to be_nil
				expect( result[:last_contacted] ).to eq( node.last_contacted.iso8601 )
				expect( result[:status_changed] ).to eq( node.status_changed.iso8601 )
				expect( result[:status_last_changed] ).to eq( node.status_last_changed.iso8601 )
				expect( result[:errors] ).to be_a( Hash )
				expect( result[:errors] ).to be_empty
				expect( result[:dependencies] ).to be_a( Hash )
				expect( result[:quieted_reasons] ).to be_a( Hash )
				expect( result[:status_history] ).to eq( node.status_history )
				expect( result[:flapping] ).to eq( node.flapping? )

				expect( result[:children] ).to be_empty
			end


			it "can include all of its serialized children" do
				result = tree.to_h( depth: -1 )

				expect( result ).to be_a( Hash )
				expect( result ).to include(
					:identifier,
					:parent, :description, :tags, :properties, :ack, :status,
					:last_contacted, :status_changed, :errors, :quieted_reasons,
					:dependencies
				)

				expect( result[:children] ).to be_a( Hash )
				expect( result[:children].length ).to eq( 4 )

				host_a = result[:children]['host-a']
				expect( host_a ).to be_a( Hash )
				expect( host_a ).to include(
					:identifier,
					:parent, :description, :tags, :properties, :ack, :status,
					:last_contacted, :status_changed, :errors, :quieted_reasons,
					:dependencies
				)
				expect( host_a[:children].length ).to eq( 3 )
			end


			it "can include a specific depth of its children"


			it "can be reconstituted from a serialized Hash of node data" do
				hash = node.to_h
				cloned_node = concrete_class.from_hash( hash )

				expect( cloned_node ).to eq( node )
			end


			it "can be marshalled" do
				data = Marshal.dump( node )
				cloned_node = Marshal.load( data )

				expect( cloned_node ).to eq( node )
			end


			it "an ACKed node stays ACKed when serialized and restored" do
				node.update( error: "there's a fire" )
				node.acknowledge(
					message: 'We know about the fire. It rages on.',
					sender: '1986 Labyrinth David Bowie'
				)
				expect( node ).to be_acked

				restored_node = Marshal.load( Marshal.dump(node) )

				expect( restored_node ).to be_acked
			end


		end

	end


	describe "event system" do

		let( :node ) do
			concrete_class.new( 'foo' ) do
				parent 'bar'
				description "The prototypical node"
				tags :chunker, :hunky, :flippin, :hippo

				update(
					'song' => 'Around the World',
					'artist' => 'Daft Punk',
					'length' => '7:09',
					'cider' => {
						'description' => 'tasty',
						'size' => '16oz',
					},
					'sausage' => {
						'description' => 'pork',
						'size' => 'monsterous',
						'price' => {
							'units' => 1200,
							'currency' => 'usd'
						}
					},
					'music' => '80s'
				)
			end
		end


		it "generates a node.update event on update" do
			events = node.update( 'song' => "Around the World" )

			expect( events ).to be_an( Array )
			expect( events ).to all( be_a(Arborist::Event) )
			expect( events.size ).to eq( 1 )
			expect( events.first.type ).to eq( 'node.update' )
			expect( events.first.node ).to be( node )
		end


		it "generates a node.delta event when an update changes a value" do
			events = node.update(
				'song' => "Motherboard",
				'artist' => 'Daft Punk',
				'sausage' => {
					'price' => {
						'currency' => 'eur'
					}
				}
			)

			expect( events ).to be_an( Array )
			expect( events ).to all( be_a(Arborist::Event) )
			expect( events.size ).to eq( 2 )

			delta_event = events.find {|ev| ev.type == 'node.delta' }

			expect( delta_event.node ).to be( node )
			expect( delta_event.payload ).to eq({
				'properties' => {
					'song' => ['Around the World' , 'Motherboard'],
					'sausage' => {
						'price' => {
							'currency' => ['usd', 'eur']
						}
					}
				}
			})
		end


		it "generates a node.delta event when an update adds a new property" do
			events = node.update(
				'genre' => "Sclerotic Neck-Funk",
			)

			expect( events ).to be_an( Array )
			expect( events ).to all( be_a(Arborist::Event) )
			expect( events.size ).to eq( 2 )

			delta_event = events.find {|ev| ev.type == 'node.delta' }

			expect( delta_event.node ).to be( node )
			expect( delta_event.payload ).to eq({
				'properties' => {
					'genre' => [nil, 'Sclerotic Neck-Funk'],
				}
			})
		end


		it "includes status changes in delta events" do
			events = node.update( error: "Couldn't talk to it!" )
			delta_event = events.find {|ev| ev.type == 'node.delta' }

			expect( delta_event.payload ).to include( 'status' => ['up', 'down'] )
		end


		it "includes the original ack in delta events" do
			events = node.acknowledge(
				message: "I have a poisonous friend. She's living in the house.",
				sender: 'Seabound'
			)
			delta_event = events.find {|ev| ev.type == 'node.delta' }
			expect( delta_event.payload ).to include( 'status' => ['up', 'disabled'] )
			expect( delta_event.payload ).to include( 'ack' => [ nil, a_hash_including(sender: 'Seabound') ] )

			events = node.unacknowledge
			delta_event = events.find {|ev| ev.type == 'node.delta' }

			expect( delta_event.payload ).to include( 'status' => ['disabled', 'unknown'] )
			expect( delta_event.payload ).to include( 'ack' => [ a_hash_including(sender: 'Seabound'), nil ] )
		end


		it "generates a node.delta event when a node ack is updated" do
			node.update( error: 'ping failed ')
			node.acknowledge(
				message: "The last one was dead. This one is on her way.",
				sender: 'Average Trigram'
			)

			events = node.acknowledge(
				message: "000100101011111",
				sender: 'Robots'
			)
			expect( events.size ).to eq( 2 )

			delta = events.last
			expect( delta ).to be_a( Arborist::Event::NodeDelta )

			expect( delta.payload ).
				to include( 'ack' => [
					a_hash_including(sender: 'Average Trigram'), a_hash_including(sender: 'Robots')
				]
			)
		end


		it "generates a node.acked and node.delta event when a node is acked" do
			node.update( error: 'ping failed ')
			events = node.acknowledge(
				message: "The last one was dead. This one is on her way.",
				sender: 'Average Trigram'
			)

			expect( events.size ).to eq( 2 )

			expect( events.first ).to be_a( Arborist::Event::NodeAcked )
			expect( events.last ).to be_a( Arborist::Event::NodeDelta )
			expect( events.first.payload ).
				to include( ack: a_hash_including(sender: 'Average Trigram') )
			expect( events.last.payload ).
				to include( 'ack' =>  [ nil, a_hash_including(sender: 'Average Trigram') ])
			expect( events.last.payload ).to include( 'status' => ['down', 'acked'] )
		end


		it "generates a node.down and node.delta event when a node is unacked" do
			node.update( error: 'ping failed ')
			node.acknowledge(
				message: "The humans are dead.  I poked one.  It's dead.",
				sender: 'Jermaine and Brit'
			)

			events = node.unacknowledge
			expect( events.last.payload ).
				to include( 'ack' =>  [ a_hash_including(sender: 'Jermaine and Brit'), nil ])
			expect( events.last.payload ).to include( 'status' => ['acked', 'down'] )
		end
	end


	describe "subscriptions" do

		let( :node ) do
			concrete_class.new( 'foo' ) do
				parent 'bar'
				description "The prototypical node"
				tags :chunker, :hunky, :flippin, :hippo
			end
		end



		it "allows the addition of a Subscription" do
			sub = Arborist::Subscription.new {}
			node.add_subscription( sub )
			expect( node.subscriptions ).to include( sub.id )
			expect( node.subscriptions[sub.id] ).to be( sub )
		end

		it "allows the removal of a Subscription" do
			sub = Arborist::Subscription.new {}
			node.add_subscription( sub )
			node.remove_subscription( sub.id )
			expect( node.subscriptions ).to_not include( sub )
		end


		it "can find subscriptions that match a given event" do
			events = node.update( 'song' => 'Fear', 'artist' => "Mind.in.a.Box" )
			delta_event = events.find {|ev| ev.type == 'node.delta' }

			sub = Arborist::Subscription.new( 'node.delta' ) {}
			node.add_subscription( sub )

			results = node.find_matching_subscriptions( delta_event )

			expect( results.size ).to eq( 1 )
			expect( results ).to all( be_a(Arborist::Subscription) )
			expect( results.first ).to be( sub )
		end


		it "can return the identifiers of all other nodes that subscribe to it" do

		end

	end


	describe "matching" do

		let( :concrete_class ) do
			Class.new( described_class ) do
				def self::name; "TestNode"; end
			end
		end


		let( :node ) do
			concrete_class.new( 'foo' ) do
				parent 'bar'
				description "The prototypical node"
				tags :chunker, :hunky, :flippin, :hippo
				config os: 'freebsd-10'

				update(
					'song' => 'Around the World',
					'artist' => 'Daft Punk',
					'length' => '7:09',
					'cider' => {
						'description' => 'tasty',
						'size' => '16oz',
					},
					'sausage' => {
						'description' => 'pork',
						'size' => 'monsterous',
						'price' => {
							'units' => 1200,
							'currency' => 'usd'
						}
					},
					'music' => '80s'
				)
			end
		end


		it "can be matched with its status" do
			expect( node ).to match_criteria( status: 'up' )
			expect( node ).to_not match_criteria( status: 'down' )
		end

		it "can be matched with multiple statuses" do
			expect( node ).to match_criteria( status: ['up','warn'] )
			expect( node ).to_not match_criteria( status: 'down' )
			expect( node ).to match_criteria( status: 'up' )
		end


		it "can be matched with its type" do
			expect( node ).to match_criteria( type: 'testnode' )
			expect( node ).to_not match_criteria( type: 'service' )
		end

		it "can be matched with its family" do
			expect( node ).to match_criteria( family: 'node' )
			expect( node ).to_not match_criteria( family: 'hurrrgh' )
		end

		it "can be matched with its parent" do
			expect( node ).to match_criteria( parent: 'bar' )
			expect( node ).to match_criteria( parent: [ 'bar', 'hooowat' ] )
			expect( node ).to_not match_criteria( parent: 'hooowat' )
			expect( node ).to_not match_criteria( parent: [ 'hooowat', 'wathoooo' ] )
		end


		it "can be matched with a single tag" do
			expect( node ).to match_criteria( tag: 'hunky' )
			expect( node ).to_not match_criteria( tag: 'plucky' )
		end


		it "can be matched with multiple tags" do
			expect( node ).to match_criteria( tags: ['hunky', 'hippo'] )
			expect( node ).to_not match_criteria( tags: ['hunky', 'hippo', 'haggis'] )
		end


		it "can be matched with its identifier" do
			expect( node ).to match_criteria( identifier: 'foo' )
			expect( node ).to_not match_criteria( identifier: 'bar' )
		end


		it "can be matched with config values" do
			expect( node ).to match_criteria( config: {os: 'freebsd-10'} )
			expect( node ).to_not match_criteria( config: {os: 'macosx-10.11.3'} )
		end


		it "can be matched with its user properties" do
			expect( node ).to match_criteria( song: 'Around the World' )
			expect( node ).to match_criteria( artist: 'Daft Punk' )
			expect( node ).to match_criteria(
				sausage: {size: 'monsterous', price: {currency: 'usd'}},
				cider: { description: 'tasty'}
			)

			expect( node ).to_not match_criteria( length: '8:01' )
			expect( node ).to_not match_criteria(
				sausage: {size: 'lunch', price: {currency: 'usd'}},
				cider: { description: 'tasty' }
			)
			expect( node ).to_not match_criteria( sausage: {size: 'lunch'} )
			expect( node ).to_not match_criteria( other: 'key' )
			expect( node ).to_not match_criteria( sausage: 'weißwürst' )
		end

	end


	describe "secondary dependencies" do

		let( :provider_node_parent ) do
			concrete_class.new( 'san' )
		end

		let( :provider_node ) do
			concrete_class.new( 'san-iscsi' ) do
				parent 'san'
			end
		end

		let( :node ) do
			concrete_class.new( 'appserver' ) do
				description "An appserver virtual machine"
			end
		end

		let( :manager ) do
			man = Arborist::Manager.new
			man.load_tree([ node, provider_node, provider_node_parent ])
			man
		end


		it "can be declared for a node" do
			node.depends_on( 'san-iscsi' )
			expect( node ).to have_dependencies
			expect( node.dependencies ).to include( 'san-iscsi' )
		end


		it "can't be declared for the root node" do
			expect {
				node.depends_on( '_' )
			}.to raise_exception( Arborist::ConfigError, /root node/i )
		end


		it "can't be declared for itself" do
			expect {
				node.depends_on( 'appserver' )
			}.to raise_exception( Arborist::ConfigError, /itself/i )
		end


		it "can't be declared for any of its ancestors" do
			provider_node.depends_on( 'san' )

			expect {
				provider_node.register_secondary_dependencies( manager )
			}.to raise_exception( Arborist::ConfigError, /ancestor/i )
		end


		it "can't be declared for any of its decendants" do
			provider_node_parent.depends_on( 'san-iscsi' )

			expect {
				provider_node_parent.register_secondary_dependencies( manager )
			}.to raise_exception( Arborist::ConfigError, /descendant/i )
		end


		it "can be declared with a simple identifier" do
			node.depends_on( 'san-iscsi' )

			expect {
				node.register_secondary_dependencies( manager )
			}.to_not raise_exception
		end


		it "can be declared on a service on a host"  do
			node.depends_on( 'iscsi', on: 'san' )
			expect( node ).to have_dependencies
			expect( node.dependencies.behavior ).to eq( :all )
			expect( node.dependencies.identifiers ).to include( 'san-iscsi' )
		end


		it "can be declared for all of a group of identifiers" do
			node.depends_on( 'iscsi', 'memcached', 'ldap', on: 'dmz' )
			expect( node ).to have_dependencies
			expect( node.dependencies.behavior ).to eq( :all )
			expect( node.dependencies.identifiers ).to include( 'dmz-iscsi', 'dmz-memcached', 'dmz-ldap' )
		end


		it "can be declared for any of a group of identifiers" do
			node.depends_on( node.any_of('memcached', on: %w[blade1 blade2 blade3]) )
			expect( node ).to have_dependencies
			expect( node.dependencies.behavior ).to eq( :all )
			expect( node.dependencies.subdeps.size ).to eq( 1 )
			subdep = node.dependencies.subdeps.first
			expect( subdep.behavior ).to eq( :any )
			expect( subdep.identifiers ).
				to include( 'blade1-memcached', 'blade2-memcached', 'blade3-memcached' )
		end


		it "cause the node to be quieted when the dependent node goes down" do
			node.depends_on( provider_node.identifier )
			node.register_secondary_dependencies( manager )

			events = provider_node.update( error: "fatal disk error: offlined" )
			provider_node.publish_events( *events )

			expect( node ).to be_quieted
			expect( node ).to have_downed_dependencies
			# :TODO: Quieted description?
		end


		it "broadcasts events generated by handled event transitions" do
			vmhost01 = concrete_class.new( 'vmhost01' )
			vm01 = concrete_class.new( 'vm01' ) do
				parent 'vmhost01'
			end
			memcache = described_class.new( 'memcache' ) do
				parent 'vm01'
			end

			mgr = Arborist::Manager.new
			mgr.load_tree([ vmhost01, vm01, memcache ])

			events = vmhost01.
				acknowledge( message: "Imma gonna f up yo' sash", sender: "GOD" )
			vmhost01.publish_events( *events )

			expect( memcache ).to be_quieted
		end

	end


	describe "operational attributes" do

		let( :node ) do
			concrete_class.new( 'foo' ) do
				parent 'bar'
				config boop: false
				description "The prototypical node"
				tags :chunker, :hunky, :flippin, :hippo
			end
		end


		it "can change its parent" do
			node.modify( parent: 'foo' )
			expect( node.parent ).to eq( 'foo' )
		end


		it "can change its description" do
			node.modify( description: 'A different node' )
			expect( node.description ).to eq( 'A different node' )
		end


		it "can change any custom configuration values" do
			node.modify( config: { boop: true } )
			expect( node.config ).to eq({ 'boop' => true })
		end


		it "can change its tags" do
			node.modify( tags: %w[dew dairy daisy dilettante] )
			expect( node.tags ).to eq( %w[dew dairy daisy dilettante] )
		end


		it "arrayifies tags modifications" do
			node.modify( tags: 'single' )
			expect( node.tags ).to eq( %w[single] )
		end


		it "knows what its operational values are" do
			values = node.operational_values

			expect( values ).to be_a( Hash )
			expect( values ).to include(
				type: node.type,
				status: node.status,
				tags: an_instance_of( Array ),
				parent: an_instance_of( String ),
				description: an_instance_of( String ),
				dependencies: an_instance_of( Arborist::Dependency ),
				status_changed: an_instance_of( Time ),
				status_last_changed: an_instance_of( Time ),
				last_contacted: an_instance_of( Time ),
				ack: nil,
				errors: an_instance_of( Hash ),
				warnings: an_instance_of( Hash ),
				quieted_reasons: an_instance_of( Hash ),
				config: an_instance_of( Hash )
			)
		end

	end


	describe "reparenting" do

		before( :each ) do
			@old_parent = concrete_class.new( 'router1' ) do
				description "The first router"
			end
			@new_parent = concrete_class.new( 'router2' ) do
				description "The second router"
			end
			@node = concrete_class.new( 'foo' ) do
				parent 'router1'
				description "The prototypical node"
			end

			@old_parent.add_child( @node )
		end

		let( :node ) { @node }
		let( :old_parent ) { @old_parent }
		let( :new_parent ) { @new_parent }


		it "moves itself to the new node and removes itself from its old parent" do
			expect( old_parent.children ).to include( node.identifier )
			expect( new_parent.children ).to_not include( node.identifier )

			node.reparent( old_parent, new_parent )

			expect( old_parent.children ).to_not include( node.identifier )
			expect( new_parent.children ).to include( node.identifier )
		end


		it "sets its state to unknown if it was down prior to the move" do
			node.update( error: 'Rock and Roll McDonalds' )

			node.reparent( old_parent, new_parent )

			expect( node ).to be_unknown
		end


		it "sets its state to unknown if it was quieted by its parent prior to the move" do
			node.quieted_reasons[ :primary ] = "Timex takes a licking and... well, broke, it looks like."
			node.status = 'quieted'

			node.reparent( old_parent, new_parent )

			expect( node ).to be_unknown
		end


		it "keeps its quieted state if it was quieted by secondary dependency prior to the move" do
			node.quieted_reasons[ :primary ] = "Timex takes a licking and... well, broke, it looks like."
			node.quieted_reasons[ :secondary ] = "Western Union:  The fastest way to send money"
			node.status = 'quieted'

			node.reparent( old_parent, new_parent )

			expect( node ).to be_quieted
		end


		it "keeps its disabled state" do
			node.acknowledge( message: 'Moving the machine', sender: 'Me' )
			expect( node ).to be_disabled

			node.reparent( old_parent, new_parent )

			expect( node ).to be_disabled
		end


		it "keeps its acked state" do
			node.update( {error: 'Batman whooped my ass.'}, 'gotham' )
			node.acknowledge( message: 'Moving the machine', sender: 'Me' )
			expect( node ).to be_acked

			node.reparent( old_parent, new_parent )

			expect( node ).to be_acked
		end

	end

end