from std/internals import setprop;
from std/path/z import ZPath;
from test/more import *;

setprop( "paths", ZPath );

let report := {
	meta: {
		title: "Before",
		status: "draft",
	},
	users: [
		{
			name: "Ada",
			role: "admin",
			active: true,
			skills: [ "math", "logic" ],
		},
		{
			name: "Bob",
			role: "reader",
			active: false,
			skills: [ "ops" ],
		},
		{
			name: "Cara",
			role: "reader",
			active: true,
			skills: [ "docs", "qa" ],
		},
	],
	params: {{ tag: "perl", tag: "zuzu", page: 1 }},
};

let focused_title := report @ "/meta/title" := "After";
is( focused_title, "After", "@ assignment returns assigned value" );
is( report{meta}{title}, "After", "@ assignment updates dict target" );

let focused_name := report @ "/users/*/name" := "ALPHA";
is( focused_name, "ALPHA", "@ assignment through wildcard returns assigned value" );
is( report{users}[0]{name}, "ALPHA", "@ assignment updates first matching node" );
is( report{users}[1]{name}, "Bob", "@ assignment leaves later wildcard matches alone" );
is( report{users}[2]{name}, "Cara", "@ assignment keeps final wildcard match unchanged" );

let filtered_age := report @ "/users/*[name == \"Bob\"]/skills/#0" := "deploy";
is( filtered_age, "deploy", "@ assignment works through filter-selected node" );
is( report{users}[1]{skills}[0], "deploy", "@ assignment mutates filter-selected array element" );

let bulk_roles := report @@ "/users/*/role" := "member";
is( bulk_roles, "member", "@@ assignment returns assigned value" );
is( report{users}[0]{role}, "member", "@@ assignment updates first role" );
is( report{users}[1]{role}, "member", "@@ assignment updates second role" );
is( report{users}[2]{role}, "member", "@@ assignment updates third role" );

let bulk_status := report @@ "/users/*[name != \"Bob\"]/role" := "active-member";
is( bulk_status, "active-member", "@@ assignment works with filtered multi-match targets" );
is( report{users}[0]{role}, "active-member", "@@ assignment updates first filtered match" );
is( report{users}[1]{role}, "member", "@@ assignment leaves filtered-out nodes unchanged" );
is( report{users}[2]{role}, "active-member", "@@ assignment updates later filtered match" );

let nested_skill := report @ "/users/#2/skills/#1" := "testing";
is( nested_skill, "testing", "@ assignment reaches nested array index target" );
is( report{users}[2]{skills}[1], "testing", "@ assignment mutates nested array index target" );

let bulk_skill_heads := report @@ "/users/*/skills/#0" := "core";
is( bulk_skill_heads, "core", "@@ assignment updates repeated nested array targets" );
is( report{users}[0]{skills}[0], "core", "@@ assignment updates first nested array head" );
is( report{users}[1]{skills}[0], "core", "@@ assignment updates second nested array head" );
is( report{users}[2]{skills}[0], "core", "@@ assignment updates third nested array head" );

let bulk_miss := report @@ "/users/*[name == \"Nobody\"]/role" := "ghost";
is( bulk_miss, "ghost", "@@ assignment with no matches still returns assigned value" );
is( report{users}[0]{role}, "active-member", "@@ no-match leaves first user unchanged" );
is( report{users}[1]{role}, "member", "@@ no-match leaves second user unchanged" );
is( report{users}[2]{role}, "active-member", "@@ no-match leaves third user unchanged" );

let focused_miss := exception( function () {
	report @ "/users/*[name == \"Nobody\"]/role" := "ghost";
} );
isnt( focused_miss, null, "@ assignment throws when no matches are found" );
like( focused_miss{message}, /Path assignment \(@\) found no matches/, "@ no-match exception message is stable" );

function fresh_compound_report () {
	return {
		meta: {
			plus: 8,
			minus: 8,
			mul: 4,
			div: 8,
			pow: 2,
			maybe: null,
			text: "AB",
			title: "Read 2026",
		},
		users: [
			{
				plus: 1,
				minus: 5,
				mul: 2,
				div: 8,
				pow: 2,
				maybe: null,
				text: "A",
				name: "Ada 10",
			},
			{
				plus: 2,
				minus: 7,
				mul: 3,
				div: 12,
				pow: 3,
				maybe: null,
				text: "B",
				name: "Bob 20",
			},
		],
	};
}

{
	let sample := fresh_compound_report();
	is( sample @ "/meta/plus" += 2, 10, "@ += returns new value" );
	is( sample{meta}{plus}, 10, "@ += mutates focused target" );
}

{
	let sample := fresh_compound_report();
	is( sample @ "/meta/minus" -= 3, 5, "@ -= returns new value" );
	is( sample{meta}{minus}, 5, "@ -= mutates focused target" );
}

{
	let sample := fresh_compound_report();
	is( sample @ "/meta/mul" *= 3, 12, "@ *= returns new value" );
	is( sample{meta}{mul}, 12, "@ *= mutates focused target" );
}

{
	let sample := fresh_compound_report();
	is( sample @ "/meta/mul" ×= 3, 12, "@ ×= returns new value" );
	is( sample{meta}{mul}, 12, "@ ×= mutates focused target" );
}

{
	let sample := fresh_compound_report();
	is( sample @ "/meta/div" /= 2, 4, "@ /= returns new value" );
	is( sample{meta}{div}, 4, "@ /= mutates focused target" );
}

{
	let sample := fresh_compound_report();
	is( sample @ "/meta/div" ÷= 2, 4, "@ ÷= returns new value" );
	is( sample{meta}{div}, 4, "@ ÷= mutates focused target" );
}

{
	let sample := fresh_compound_report();
	is( sample @ "/meta/pow" **= 3, 8, "@ **= returns new value" );
	is( sample{meta}{pow}, 8, "@ **= mutates focused target" );
}

{
	let sample := fresh_compound_report();
	is( sample @ "/meta/text" _= "!", "AB!", "@ _= returns new value" );
	is( sample{meta}{text}, "AB!", "@ _= mutates focused target" );
}

{
	let sample := fresh_compound_report();
	is( sample @ "/meta/maybe" ?:= 9, 9, "@ ?:= returns assigned value when target is null" );
	is( sample{meta}{maybe}, 9, "@ ?:= mutates focused null target" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/users/*/plus" += 2, 2, "@@ += returns RHS contract" );
	is( sample{users}[0]{plus}, 3, "@@ += mutates first selected target" );
	is( sample{users}[1]{plus}, 4, "@@ += mutates later selected target" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/users/*/minus" -= 2, 2, "@@ -= returns RHS contract" );
	is( sample{users}[0]{minus}, 3, "@@ -= mutates first selected target" );
	is( sample{users}[1]{minus}, 5, "@@ -= mutates later selected target" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/users/*/mul" *= 2, 2, "@@ *= returns RHS contract" );
	is( sample{users}[0]{mul}, 4, "@@ *= mutates first selected target" );
	is( sample{users}[1]{mul}, 6, "@@ *= mutates later selected target" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/users/*/mul" ×= 2, 2, "@@ ×= returns RHS contract" );
	is( sample{users}[0]{mul}, 4, "@@ ×= mutates first selected target" );
	is( sample{users}[1]{mul}, 6, "@@ ×= mutates later selected target" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/users/*/div" /= 2, 2, "@@ /= returns RHS contract" );
	is( sample{users}[0]{div}, 4, "@@ /= mutates first selected target" );
	is( sample{users}[1]{div}, 6, "@@ /= mutates later selected target" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/users/*/div" ÷= 2, 2, "@@ ÷= returns RHS contract" );
	is( sample{users}[0]{div}, 4, "@@ ÷= mutates first selected target" );
	is( sample{users}[1]{div}, 6, "@@ ÷= mutates later selected target" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/users/*/pow" **= 2, 2, "@@ **= returns RHS contract" );
	is( sample{users}[0]{pow}, 4, "@@ **= mutates first selected target" );
	is( sample{users}[1]{pow}, 9, "@@ **= mutates later selected target" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/users/*/text" _= "!", "!", "@@ _= returns RHS contract" );
	is( sample{users}[0]{text}, "A!", "@@ _= mutates first selected target" );
	is( sample{users}[1]{text}, "B!", "@@ _= mutates later selected target" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/users/*/maybe" ?:= 7, 7, "@@ ?:= returns RHS contract" );
	is( sample{users}[0]{maybe}, 7, "@@ ?:= mutates first selected null target" );
	is( sample{users}[1]{maybe}, 7, "@@ ?:= mutates later selected null target" );
}

{
	let sample := fresh_compound_report();
	ok( sample @? "/meta/plus" += 2, "@? += returns true on match" );
	is( sample{meta}{plus}, 10, "@? += mutates matched target" );
}

{
	let sample := fresh_compound_report();
	ok( sample @? "/meta/minus" -= 3, "@? -= returns true on match" );
	is( sample{meta}{minus}, 5, "@? -= mutates matched target" );
}

{
	let sample := fresh_compound_report();
	ok( sample @? "/meta/mul" *= 3, "@? *= returns true on match" );
	is( sample{meta}{mul}, 12, "@? *= mutates matched target" );
}

{
	let sample := fresh_compound_report();
	ok( sample @? "/meta/mul" ×= 3, "@? ×= returns true on match" );
	is( sample{meta}{mul}, 12, "@? ×= mutates matched target" );
}

{
	let sample := fresh_compound_report();
	ok( sample @? "/meta/div" /= 2, "@? /= returns true on match" );
	is( sample{meta}{div}, 4, "@? /= mutates matched target" );
}

{
	let sample := fresh_compound_report();
	ok( sample @? "/meta/div" ÷= 2, "@? ÷= returns true on match" );
	is( sample{meta}{div}, 4, "@? ÷= mutates matched target" );
}

{
	let sample := fresh_compound_report();
	ok( sample @? "/meta/pow" **= 3, "@? **= returns true on match" );
	is( sample{meta}{pow}, 8, "@? **= mutates matched target" );
}

{
	let sample := fresh_compound_report();
	ok( sample @? "/meta/text" _= "!", "@? _= returns true on match" );
	is( sample{meta}{text}, "AB!", "@? _= mutates matched target" );
}

{
	let sample := fresh_compound_report();
	ok( sample @? "/meta/maybe" ?:= 9, "@? ?:= returns true on match" );
	is( sample{meta}{maybe}, 9, "@? ?:= mutates matched target" );
}

{
	let sample := fresh_compound_report();
	is(
		sample @ "/meta/title" ~= /([A-Za-z]+) ([0-9]+)/ -> `${m[1]}:${m[2]}`,
		"Read:2026",
		"@ ~= supports captures",
	);
	is( sample{meta}{title}, "Read:2026", "@ ~= mutates focused target" );
}

{
	let sample := fresh_compound_report();
	is(
		sample @@ "/users/*/name" ~= /([A-Za-z]+) ([0-9]+)/ -> do {
			let who := m[1];
			let digits := m[2];
			who _ ":" _ digits;
		},
		"Bob:20",
		"@@ ~= returns last replaced value",
	);
	is( sample{users}[0]{name}, "Ada:10", "@@ ~= mutates first selected target" );
	is( sample{users}[1]{name}, "Bob:20", "@@ ~= mutates later selected target" );
}

{
	let sample := fresh_compound_report();
	ok(
		sample @? "/meta/title" ~= /([A-Za-z]+) ([0-9]+)/ -> `${m[1]}:${m[2]}`,
		"@? ~= returns true on match",
	);
	is( sample{meta}{title}, "Read:2026", "@? ~= mutates matched target" );
}

{
	let sample := fresh_compound_report();
	let miss := exception( function () {
		sample @ "/meta/missing" += 2;
	} );
	isnt( miss, null, "@ += throws on selector miss" );
}

{
	let sample := fresh_compound_report();
	is( sample @@ "/meta/missing" += 2, 2, "@@ += no-match still returns RHS" );
	is( sample{meta}{plus}, 8, "@@ += no-match leaves structure unchanged" );
}

{
	let sample := fresh_compound_report();
	is( sample @? "/meta/missing" += 2, false, "@? += returns false on selector miss" );
	is( sample{meta}{plus}, 8, "@? += no-match leaves structure unchanged" );
}

{
	let sample := fresh_compound_report();
	let miss := exception( function () {
		sample @ "/meta/missing" ~= /x/ -> "y";
	} );
	isnt( miss, null, "@ ~= throws on selector miss" );
}

{
	let sample := fresh_compound_report();
	sample @@ "/meta/missing" ~= /x/ -> "y";
	is( sample{meta}{title}, "Read 2026", "@@ ~= no-match leaves structure unchanged" );
}

{
	let sample := fresh_compound_report();
	is( sample @? "/meta/missing" ~= /x/ -> "y", false, "@? ~= returns false on selector miss" );
	is( sample{meta}{title}, "Read 2026", "@? ~= no-match leaves structure unchanged" );
}

done_testing();
