from std/data/kdl import KDL, KDLNode;
from std/path/kdl import KDLQuery;
from std/string import chr;
from test/more import *;

let doc := ( new KDL() ).decode( """(pkg)package {
	name foo
	version "1.0.0"
	dependencies platform=windows {
		winapi "1.0.0" path="./crates/my-winapi-fork"
	}
	dependencies {
		miette "2.0.0" dev=#true integrity=(sri)sha512-deadbeef
	}
}
""" );

function query ( d, p ) {
	return ( new KDLQuery( path: p ) ).query(d);
}

function first ( d, p, fallback ) {
	return ( new KDLQuery( path: p ) ).first( d, fallback );
}

function exists ( d, p ) {
	return ( new KDLQuery( path: p ) ).exists(d);
}

let names := query( doc, "package >> name" );
is( names.length(), 1, "descendant selector returns matching node" );
is( names[0].name(), "name", "descendant selector keeps node object" );

is(
	first( doc, "top() > package >> name", null ).name(),
	"name",
	"top() constrains selector to document root",
);

let deps := query( doc, "dependencies" );
is( deps.length(), 2, "plain selector deep-fetches matching nodes" );

let platform_deps := query( doc, "dependencies[platform]" );
is( platform_deps.length(), 1, "plain property accessor filters nodes" );
is(
	platform_deps[0].props().get("platform").native_value(),
	"windows",
	"property filter keeps matching dependency",
);

is(
	query( doc, "dependencies[prop(platform)]" ).length(),
	1,
	"prop(name) accessor matches plain property accessor",
);

is(
	query( doc, "dependencies > []" ).map( fn n -> n.name() ),
	[ "winapi", "miette" ],
	"child combinator with wildcard returns direct children",
);

is(
	query( doc, "version + dependencies" ).map( fn n -> n.name() ),
	[ "dependencies" ],
	"next-sibling combinator returns immediate sibling",
);

is(
	query( doc, "name ++ dependencies" ).length(),
	2,
	"following-sibling combinator returns later siblings",
);

is(
	query( doc, "dependencies > winapi || dependencies > miette" )
		.map( fn n -> n.name() ),
	[ "winapi", "miette" ],
	"selector union combines results",
);

is(
	first( doc, "(pkg)package", null ).name(),
	"package",
	"type matcher selects node annotation",
);
is(
	first( doc, "()[tag() = pkg]", null ).name(),
	"package",
	"tag accessor compares node annotation",
);
is(
	first( doc, "[name() = package]", null ).name(),
	"package",
	"name accessor compares node name",
);

is(
	query( doc, "dependencies > winapi[val() = \"1.0.0\"]" ).length(),
	1,
	"val accessor compares first argument",
);
is(
	query( doc, "dependencies > miette[dev = #true]" ).length(),
	1,
	"boolean property comparison works",
);
is(
	query( doc, "dependencies > miette[integrity = (sri)]" ).length(),
	1,
	"type literal compares value annotation",
);

is(
	query( doc, "dependencies > [val() ^= \"1.\"]" ).map( fn n -> n.name() ),
	[ "winapi" ],
	"string prefix comparison works",
);
is(
	query( doc, "dependencies > [val() $= \"0\"]" ).map( fn n -> n.name() ),
	[ "winapi", "miette" ],
	"string suffix comparison works",
);
is(
	query( doc, "dependencies > [val() *= \".0.\"]" ).map( fn n -> n.name() ),
	[ "winapi", "miette" ],
	"string contains comparison works",
);

ok(
	exists( doc, "dependencies > miette[values()]" ),
	"values() existence accessor",
);
ok(
	exists( doc, "dependencies > miette[props()]" ),
	"props() existence accessor",
);
ok(
	not exists( doc, "dependencies > winapi[dev]" ),
	"exists returns false on miss",
);

let compiled := new KDLQuery( path: "package >> name" );
is(
	compiled.expression(),
	"package >> name",
	"expression returns source path",
);
is( doc @ compiled, names[0], "@ works with compiled KDLQuery" );
is(
	( doc @@ new KDLQuery( path: "package >> dependencies > []" ) ).length(),
	2,
	"@@ works with compiled KDLQuery",
);
ok(
	doc @? new KDLQuery( path: "dependencies[platform]" ),
	"@? works with compiled KDLQuery",
);

let native_data := {
	package: {
		name: "zuzu",
		dependencies: [
			{ name: "tap" },
			{ name: "unicode" },
		],
	},
};
is(
	query( native_data, "package[name = \"zuzu\"]" ).length(),
	1,
	"non-KDL root is converted with json_to_kdl",
);
is(
	query( native_data, "dependencies > -[name = \"unicode\"]" ).length(),
	1,
	"converted native arrays can be queried",
);

KDLQuery.use();
is(
	( doc @@ "package >> name" )[0].name(),
	"name",
	"KDLQuery.use registers string path operators",
);

let features := ( new KDL() ).decode( """alpha
beta
gamma
raw #"hello"#
hex 0x10
float 1.5e2
posinf #inf
multi 1 "x" flag=#true score=2
#"weird name"# "ok"
"snow\u{2603}" "cold"
""" );

is(
	query( features, "alpha + beta" ).map( fn n -> n.name() ),
	[ "beta" ],
	"next-sibling combinator works at document root",
);
is(
	query( features, "alpha ++ gamma" ).map( fn n -> n.name() ),
	[ "gamma" ],
	"following-sibling combinator works at document root",
);
is(
	query( features, "raw[val() = #\"hello\"#]" ).length(),
	1,
	"raw string literals compare as KDL strings",
);
is(
	query( features, "hex[val() = 0x10]" ).length(),
	1,
	"non-decimal integer literals compare as KDL numbers",
);
is(
	query( features, "float[val() = 1.5e2]" ).length(),
	1,
	"exponent float literals compare as KDL numbers",
);
is(
	query( features, "posinf[val() = #inf]" ).length(),
	1,
	"special number keywords compare by KDL value",
);
is(
	query( features, "multi[values() = \"x\"]" ).length(),
	1,
	"values() comparisons are existential over node args",
);
is(
	query( features, "multi[props() = 2]" ).length(),
	1,
	"props() comparisons are existential over property values",
);
is(
	query( features, "#\"weird name\"#" )[0].name(),
	"weird name",
	"raw string node names use KDL string parsing",
);
is(
	query( features, "\"snow\\u{2603}\"" )[0].name(),
	"snow" _ chr(9731),
	"quoted string node names use KDL Unicode escapes",
);
is(
	( new KDLQuery( path: "multi" ) ).values(features)[1].native_value(),
	"x",
	"values helper extracts selected node arguments",
);
is(
	( new KDLQuery( path: "multi" ) ).props(features).get("score").native_value(),
	2,
	"props helper extracts selected node properties",
);

let mutable := ( new KDL() ).decode( """root {
	old
	again
}
""" );
let replacement := new KDLNode( name: "new" );
is(
	( new KDLQuery( path: "root > old" ) ).assign_first(
		mutable,
		replacement,
	),
	replacement,
	"assign_first returns replacement node",
);
is(
	query( mutable, "root > new" ).length(),
	1,
	"assign_first replaces selected child node",
);
let ref := ( new KDLQuery( path: "root > new" ) ).ref_first(mutable);
ref( new KDLNode( name: "refd" ) );
is(
	query( mutable, "root > refd" ).length(),
	1,
	"ref_first returns an assignable node reference",
);
let op_replacement := new KDLNode( name: "op" );
let op_query := new KDLQuery( path: "root > again" );
let op_result := mutable @ op_query := op_replacement;
is(
	op_result,
	op_replacement,
	"path operator assignment returns replacement node",
);
is(
	query( mutable, "root > op" ).length(),
	1,
	"path operator assignment replaces selected child node",
);

done_testing();
