from test/more import *;

requires_capability( "fs" );

from std/archive import Archive;
from std/data/json import JSON;
from std/io import Path;
from std/zuzuzoo import Zuzuzoo, dependency_roots, find_dependency;

function throws_like ( f, rx, name ) {
	let got := "";
	try {
		f();
	}
	catch ( Exception e ) {
		got := e.to_String();
	}
	like( got, rx, name );
}

function ensure_dir ( path ) {
	return true if path.exists();
	let parent := path.parent();
	ensure_dir(parent) if not parent.exists();
	path.mkdir();
	return true;
}

function root ( name ) {
	let dir := Path.tempdir();
	let lib := dir.child("lib");
	let bin := dir.child("bin");
	let meta := dir.child("meta");
	ensure_dir(lib);
	ensure_dir(bin);
	ensure_dir(meta);
	return {
		name: name,
		dir_obj: dir,
		lib_obj: lib,
		bin_obj: bin,
		meta_obj: meta,
		lib_dir: lib.to_String(),
		bin_dir: bin.to_String(),
		meta_dir: meta.to_String(),
	};
}

class FakeRequest {
	let target_url := null;
	let download_path := null;

	method download_to ( path ) {
		download_path := path;
		return self;
	}

	method url () {
		return target_url;
	}

	method path () {
		return download_path;
	}
}

class FakeResponse {
	method expect_success () {
		return self;
	}

	method url () {
		return "";
	}
}

class FakeUserAgent {
	let payloads := {};
	let urls := [];

	method build_request ( http_method, url ) {
		urls.push(url);
		return new FakeRequest( target_url: url );
	}

	method send ( request ) {
		( new Path( request.path() ) ).spew(
			payloads.get( request.url() ),
		);
		return new FakeResponse();
	}

	method seen () {
		return urls;
	}
}

function write_installed ( root, dist_name, version, module_name ) {
	( new JSON( pretty: true ) ).dump(
		( new Path( root{meta_dir} ) ).child(
			dist_name _ "-" _ version _ ".json",
		),
		{
			name: dist_name,
			version: version,
			author: "Example",
			license: "MIT",
			status: "stable",
			dependencies: {},
			installed: {
				zdf: "ZDF-1",
				lib_dir: root{lib_dir},
				bin_dir: root{bin_dir},
				meta_dir: root{meta_dir},
				modules: [
					{
						source: "modules/" _ module_name _ ".zzm",
						install_as: module_name _ ".zzm",
						sha256: "abc",
					},
				],
				scripts: [],
			},
		},
	);
}

function archive_path ( dist_name, module_name, dependency_name ) {
	let path := Path.tempfile();
	Archive.dump(
		path,
		{
			entries: [
				{
					path: dist_name _ "-1.0.0/zuzu-distribution.json",
					data: to_binary(
						"{\"name\":\"" _ dist_name _
						"\",\"version\":\"1.0.0\"," _
						"\"author\":\"Example\"," _
						"\"license\":\"MIT\"," _
						"\"status\":\"stable\"," _
						"\"dependencies\":{\"" _
						dependency_name _ "\":\"1.0.0\"}}\n",
					),
				},
				{
					path: dist_name _ "-1.0.0/modules/" _
						module_name _ ".zzm",
					data: to_binary("// module\n"),
				},
			],
		},
		"zip",
	);
	return path;
}

function planning_zoo (
	target_root,
	home,
	ua,
	extra_roots := [],
	builtin_global_root := null
) {
	return new Zuzuzoo(
		lib_dir: target_root{lib_dir},
		bin_dir: target_root{bin_dir},
		meta_dir: target_root{meta_dir},
		home: home.to_String(),
		base_url: "https://packages.test",
		user_agent: ua,
		dependency_roots: extra_roots,
		global_root: builtin_global_root,
	);
}

function assert_plan_satisfied_by ( root_name, installed_root, extra_roots := [] ) {
	let plan_target := root("plan-" _ root_name);
	plan_target := installed_root if root_name eq "target";
	let plan_home := Path.tempdir();
	let ua := new FakeUserAgent();
	let dep_name := "satisfied/" _ root_name;
	write_installed(
		installed_root,
		"installed-" _ root_name,
		"1.0.0",
		dep_name,
	);
	let app := archive_path(
		"app-" _ root_name,
		"app/" _ root_name,
		dep_name,
	);
	let install_plan := planning_zoo(
		plan_target,
		plan_home,
		ua,
		extra_roots,
	).plan_install(app.to_String());
	is(
		install_plan{installs}.length(),
		1,
		root_name _ " dependency is not planned for install",
	);
	is(
		install_plan{satisfied_dependencies}[0]{distribution},
		"installed-" _ root_name,
		root_name _ " dependency is satisfied from metadata",
	);
	is(
		install_plan{satisfied_dependencies}[0]{root}{name},
		root_name,
		root_name _ " dependency records the satisfying root",
	);
	is(
		ua.seen(),
		[],
		root_name _ " satisfied dependency is not fetched",
	);
}

let target := root("target");
let ctor := root("ctor");
let option := root("option");
let home := Path.tempdir();

let zoo := new Zuzuzoo(
	lib_dir: target{lib_dir},
	bin_dir: target{bin_dir},
	meta_dir: target{meta_dir},
	home: home.to_String(),
	dependency_roots: [ ctor ],
);
let roots := zoo.dependency_roots( { dependency_roots: [ option ] } );
is(
	roots.map( fn r -> r{name} ),
	[ "target", "global", "user", "ctor", "option" ],
	"dependency roots are returned in target/global/user/custom order",
);

is(
	dependency_roots(
		{
			lib_dir: target{lib_dir},
			bin_dir: target{bin_dir},
			meta_dir: target{meta_dir},
			home: home.to_String(),
		},
	)[0]{name},
	"target",
	"top-level dependency_roots wrapper returns roots",
);

throws_like(
	function () {
		zoo.dependency_roots(
			{
				dependency_roots: [
					{
						name: "bad",
						lib_dir: "/tmp/lib",
						meta_dir: "/tmp/meta",
					},
				],
			},
		);
	},
	/missing bin_dir/,
	"custom dependency roots require bin_dir",
);

write_installed( target, "target-tie", "1.0.0", "tie/mod" );
write_installed( ctor, "ctor-tie", "1.0.0", "tie/mod" );
is(
	zoo.find_dependency("tie/mod"){distribution},
	"target-tie",
	"equal versions prefer earlier root order",
);

write_installed( target, "target-newest", "1.0.0", "new/mod" );
write_installed( ctor, "ctor-newest", "2.0.0", "new/mod" );
is(
	zoo.find_dependency("new/mod"){distribution},
	"ctor-newest",
	"newest sufficient dependency wins across roots",
);
is(
	zoo.find_dependency( "new/mod", "3.0.0" ),
	null,
	"insufficient installed dependency is ignored",
);

let user_meta := home.child(".zuzu").child("meta");
ensure_dir(user_meta);
let user_root := {
	name: "user",
	lib_dir: home.child(".zuzu").child("modules").to_String(),
	bin_dir: home.child(".zuzu").child("bin").to_String(),
	meta_dir: user_meta.to_String(),
};
write_installed( user_root, "user-dep", "1.0.0", "user/dep" );
is(
	find_dependency(
		"user/dep",
		"0",
		{
			lib_dir: target{lib_dir},
			bin_dir: target{bin_dir},
			meta_dir: target{meta_dir},
			home: home.to_String(),
		},
	){root}{name},
	"user",
	"top-level find_dependency scans default user root",
);

let plan_target := root("target");
assert_plan_satisfied_by( "target", plan_target );

let plan_global_root := root("global");
let global_plan_target := root("global-target");
let global_plan_home := Path.tempdir();
let global_ua := new FakeUserAgent();
write_installed(
	plan_global_root,
	"installed-global",
	"1.0.0",
	"satisfied/global",
);
let global_app := archive_path(
	"app-global",
	"app/global",
	"satisfied/global",
);
let global_install_plan := planning_zoo(
	global_plan_target,
	global_plan_home,
	global_ua,
	[],
	plan_global_root,
).plan_install(global_app.to_String());
is(
	global_install_plan{dependency_roots}[1]{name},
	"global",
	"temp global fixture is the built-in global dependency root",
);
is(
	global_install_plan{dependency_roots}[1]{kind},
	"global",
	"temp global fixture keeps global root kind",
);
is(
	global_install_plan{installs}.length(),
	1,
	"global dependency is not planned for install",
);
is(
	global_install_plan{satisfied_dependencies}[0]{distribution},
	"installed-global",
	"global dependency is satisfied from built-in global metadata",
);
is(
	global_install_plan{satisfied_dependencies}[0]{root}{name},
	"global",
	"global dependency records the satisfying root",
);
is( global_ua.seen(), [], "global satisfied dependency is not fetched" );

let plan_user_home := Path.tempdir();
let plan_user_meta := plan_user_home.child(".zuzu").child("meta");
ensure_dir(plan_user_meta);
let plan_user_root := {
	name: "user",
	lib_dir: plan_user_home.child(".zuzu").child("modules").to_String(),
	bin_dir: plan_user_home.child(".zuzu").child("bin").to_String(),
	meta_dir: plan_user_meta.to_String(),
};
write_installed(
	plan_user_root,
	"installed-user-plan",
	"1.0.0",
	"satisfied/user-plan",
);
let user_ua := new FakeUserAgent();
let user_app := archive_path(
	"app-user-plan",
	"app/user-plan",
	"satisfied/user-plan",
);
let user_install_plan := planning_zoo(
	root("user-target"),
	plan_user_home,
	user_ua,
).plan_install(user_app.to_String());
is(
	user_install_plan{installs}.length(),
	1,
	"user dependency is not planned for install",
);
is(
	user_install_plan{satisfied_dependencies}[0]{root}{name},
	"user",
	"user dependency records the satisfying root",
);
is( user_ua.seen(), [], "user satisfied dependency is not fetched" );

let plan_custom_root := root("custom");
assert_plan_satisfied_by(
	"custom",
	plan_custom_root,
	[ plan_custom_root ],
);

done_testing();
