from std/path/z/node import Node;
from std/path/z import ZPath;
from std/path/zz import ZZPath;
from test/more import *;

let root := Node.from_root( {
	name: "Ada",
	age: 37,
	tags: [ "a", "b" ],
	nested: {
		name: "Nested",
		child: {
			name: "Leaf",
		},
	},
} );

is( root.type(), "map", "root dict is map" );

let kids := root.children();
ok( kids.length() >= 3, "dict children are wrapped" );

let name_node := null;
for ( let kid in kids ) {
	if ( kid.key() ≡ "name" ) {
		name_node := kid;
	}
}

ok( name_node ≢ null, "name child found" );
is( name_node.type(), "string", "name child type" );
is( name_node.string_value(), "Ada", "string_value uses primitive conversion" );

let dump := root.dump();
is( dump{"@type"}, "map", "dump includes type" );

let scoped_root := Node.from_root( {
	name: "Grace",
} );
let scoped_child := scoped_root.children().first( fn kid → kid.key() ≡ "name" );
is( scoped_child.parent(), scoped_root, "child sees live parent node" );
scoped_root := null;
is( scoped_child.parent(), null, "child does not keep parent node alive" );

let string_error := exception( function () {
	root.find("/name");
} );
isnt( string_error, null, "find rejects plain path strings" );
like(
	string_error{message},
	/Node\.find expects a ZPath object/,
	"find string rejection message is stable",
);

let found_name := root.find( new ZPath( path: "/name" ) );
is( found_name.length(), 1, "find accepts compiled ZPath object" );
is( found_name[0].primitive_value(), "Ada", "find returns selected node value" );
is( found_name[0].key(), "name", "find preserves selected node key" );
is( found_name[0].parent(), root, "find preserves parent metadata" );
is(
	found_name[0].id(),
	root.id() _ "/name",
	"find preserves selected node id",
);

let zz_found_name := root.find( new ZZPath( path: "/name" ) );
is( zz_found_name.length(), 1, "find accepts ZZPath subclass object" );
is( zz_found_name[0].primitive_value(), "Ada", "find uses ZZPath subclass" );

let nested_node := root.find( new ZPath( path: "/nested" ) )[0];
is(
	nested_node.find( new ZPath( path: "/name" ) )[0].primitive_value(),
	"Nested",
	"absolute find paths are rooted at the receiver node",
);
is(
	nested_node.find( new ZPath( path: "child/name" ) )[0].primitive_value(),
	"Leaf",
	"relative find paths start from the receiver node",
);
is(
	nested_node.find( new ZPath( path: "/missing" ) ).length(),
	0,
	"find returns empty list for no match",
);

let pairlist_root := Node.from_root( {{
	tag: "perl",
	page: 1,
	tag: "zuzu",
}} );
let pairlist_children := pairlist_root.children();
is( pairlist_children.length(), 3, "pairlist children enumerate pairs" );
is( pairlist_children[0].key(), "tag", "first pairlist child keeps key" );
is( pairlist_children[0].index(), 0, "first pairlist child keeps global index" );
is( pairlist_children[1].key(), "page", "second pairlist child keeps key" );
is( pairlist_children[2].key(), "tag", "duplicate pairlist child keeps key" );
is( pairlist_children[2].index(), 2, "duplicate pairlist child keeps global index" );
is(
	pairlist_root.indexed_child(1).raw().value,
	1,
	"pairlist indexed_child uses global pair index",
);
is(
	pairlist_root.named_child("tag").raw().value,
	"perl",
	"pairlist named_child returns first matching pair",
);
is(
	pairlist_root.named_indexed_child( "tag", 1 ).raw().value,
	"zuzu",
	"pairlist named_indexed_child uses per-key occurrence index",
);

let pair_attrs := pairlist_children[0].attributes();
is( pair_attrs.length(), 2, "pair node exposes key and value attributes" );
is( pair_attrs[0].key(), "@key", "pair key attribute is named" );
is( pair_attrs[1].key(), "@value", "pair value attribute is named" );
is(
	pairlist_root.dump(){children}.length(),
	3,
	"pairlist dump includes pair children",
);

done_testing();
