local createLibTest = require("/apis/libtest")
local createCcpm = require("/apis/libccpm")

local testlib = createLibTest({ ... })

local counter = 0

-- Fresh, isolated state + isolated install per case (never touches real /trapos).
local function freshDirs()
	counter = counter + 1
	local stateDir = "/ccpm-test/state-" .. counter
	local installRoot = "/ccpm-test/root-" .. counter
	fs.delete(stateDir)
	fs.delete(installRoot)
	return stateDir, installRoot
end

-- Minimal stub of the CC `http` API backed by a url -> body map.
local function fakeHttp(routes)
	return {
		get = function(url)
			local body = routes[url]
			if not body then
				return nil
			end
			return {
				readAll = function()
					return body
				end,
				close = function() end,
			}
		end,
	}
end

local function giteaBase(name, branch)
	return "https://git.trapcloud.fr/" .. name .. "/raw/branch/" .. branch .. "/"
end

local function writeFile(path, body)
  local dir = path:match("^(.*)/[^/]+$");
  if dir then fs.makeDir(dir); end
  local f = fs.open(path, "w");
  f.write(body);
  f.close();
end

testlib.test("registryBaseUrl resolves a gitea branch", function()
	local ccpm = createCcpm({ stateDir = freshDirs() })
	testlib.assertEquals(
		ccpm.registryBaseUrl({ name = "guillaumearm/cc-libs", type = "gitea", branch = "next" }),
		"https://git.trapcloud.fr/guillaumearm/cc-libs/raw/branch/next/"
	)
	testlib.assertEquals(
		ccpm.descriptorUrl({ name = "guillaumearm/cc-libs", type = "gitea", branch = "next" }, "trapos-ui"),
		"https://git.trapcloud.fr/guillaumearm/cc-libs/raw/branch/next/packages/trapos-ui/ccpm.json"
	)
end)

testlib.test("registry urls resolve an http base", function()
	local ccpm = createCcpm({ stateDir = freshDirs() })
	testlib.assertEquals(
		ccpm.registryBaseUrl({ name = "http://example.com/repo", type = "http" }),
		"http://example.com/repo/"
	)
	testlib.assertEquals(
		ccpm.descriptorUrl({ name = "http://example.com/repo/", type = "http" }, "trapos-ui"),
		"http://example.com/repo/packages/trapos-ui/ccpm.json"
	)
end)

testlib.test("resolve orders dependencies before dependents", function()
	local base = giteaBase("me/repo", "master")
	local routes = {
		[base .. "packages/a/ccpm.json"] = textutils.serializeJSON({
			name = "a",
			version = "1",
			dependencies = { "b", "c" },
			files = {},
		}),
		[base .. "packages/b/ccpm.json"] = textutils.serializeJSON({
			name = "b",
			version = "1",
			dependencies = { "c" },
			files = {},
		}),
		[base .. "packages/c/ccpm.json"] = textutils.serializeJSON({
			name = "c",
			version = "1",
			dependencies = {},
			files = {},
		}),
	}
	local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) })
	ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } })
	local ordered, err = ccpm.resolve("a")
	testlib.assertTrue(ordered, tostring(err))
	testlib.assertEquals(#ordered, 3)
	testlib.assertEquals(ordered[1].name, "c")
	testlib.assertEquals(ordered[2].name, "b")
	testlib.assertEquals(ordered[3].name, "a")
end)

testlib.test("resolve reports a missing package", function()
	local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp({}) })
	ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } })
	local ordered, err = ccpm.resolve("ghost")
	testlib.assertTrue(not ordered)
	testlib.assertTrue(string.find(err, "not found", 1, true))
end)

testlib.test("resolve detects a dependency cycle", function()
	local base = giteaBase("me/repo", "master")
	local routes = {
		[base .. "packages/a/ccpm.json"] = textutils.serializeJSON({
			name = "a",
			version = "1",
			dependencies = { "b" },
		}),
		[base .. "packages/b/ccpm.json"] = textutils.serializeJSON({
			name = "b",
			version = "1",
			dependencies = { "a" },
		}),
	}
	local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) })
	ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } })
	local ordered, err = ccpm.resolve("a")
	testlib.assertTrue(not ordered)
	testlib.assertTrue(string.find(err, "cycle", 1, true))
end)

testlib.test("install rejects an already-installed package", function()
	local ccpm = createCcpm({ stateDir = freshDirs() })
	ccpm.writeLock({ packages = { foo = { version = "1", files = {} } } })
	local ok, msg = ccpm.install("foo", {})
	testlib.assertTrue(not ok)
	testlib.assertTrue(string.find(msg, "already installed", 1, true))
	testlib.assertTrue(string.find(msg, "reinstall foo", 1, true))
end)

testlib.test("install downloads files and records the lock", function()
	local base = giteaBase("me/repo", "master")
	local routes = {
		[base .. "packages/trapos-core/ccpm.json"] = textutils.serializeJSON({
			name = "trapos-core",
			version = "1",
			dependencies = {},
			files = { "apis/eventloop.lua" },
		}),
		[base .. "packages/trapos-ui/ccpm.json"] = textutils.serializeJSON({
			name = "trapos-ui",
			version = "1",
			dependencies = { "trapos-core" },
			files = { "apis/libtui.lua" },
		}),
		[base .. "packages/trapos-core/apis/eventloop.lua"] = "eventloop-body",
		[base .. "packages/trapos-ui/apis/libtui.lua"] = "ui-body",
	}
	local sd, root = freshDirs()
	local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) })
	ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } })

	local ok = ccpm.install("trapos-ui", {})
	testlib.assertTrue(ok)
	testlib.assertTrue(fs.exists(root .. "/apis/libtui.lua"))
	testlib.assertTrue(fs.exists(root .. "/apis/eventloop.lua"))

	local f = fs.open(root .. "/apis/libtui.lua", "r")
	local body = f.readAll()
	f.close()
	testlib.assertEquals(body, "ui-body")

	local lock = ccpm.readLock()
	testlib.assertTrue(lock.packages["trapos-ui"])
	testlib.assertTrue(lock.packages["trapos-core"])
	testlib.assertEquals(lock.packages["trapos-ui"].registry, "me/repo")
	testlib.assertEquals(lock.packages["trapos-ui"].explicit, true)
	testlib.assertEquals(lock.packages["trapos-core"].explicit, false)
end)

testlib.test("install downloads files from a gitea registry", function()
	local base = giteaBase("guillaumearm/cc-libs", "next")
	local routes = {
		[base .. "packages/trapos-core/ccpm.json"] = textutils.serializeJSON({
			name = "trapos-core",
			version = "1",
			dependencies = {},
			files = { "apis/eventloop.lua" },
		}),
		[base .. "packages/trapos-ui/ccpm.json"] = textutils.serializeJSON({
			name = "trapos-ui",
			version = "1",
			dependencies = { "trapos-core" },
			files = { "apis/libtui.lua" },
		}),
		[base .. "packages/trapos-core/apis/eventloop.lua"] = "eventloop-body",
		[base .. "packages/trapos-ui/apis/libtui.lua"] = "ui-body",
	}
	local sd, root = freshDirs()
	local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) })
	ccpm.writeConfig({ registries = { { name = "guillaumearm/cc-libs", type = "gitea", branch = "next" } } })

	local ok = ccpm.install("trapos-ui", {})
	testlib.assertTrue(ok)
	testlib.assertTrue(fs.exists(root .. "/apis/libtui.lua"))
	testlib.assertTrue(fs.exists(root .. "/apis/eventloop.lua"))

	local f = fs.open(root .. "/apis/libtui.lua", "r")
	local body = f.readAll()
	f.close()
	testlib.assertEquals(body, "ui-body")

	local lock = ccpm.readLock()
	testlib.assertEquals(lock.packages["trapos-ui"].registry, "guillaumearm/cc-libs")
end)

testlib.test("installing trapos writes aggregated os state", function()
	local base = giteaBase("me/repo", "master")
	local routes = {
		[base .. "packages/trapos/ccpm.json"] = textutils.serializeJSON({
			name = "trapos",
			version = "1",
			dependencies = { "trapos-ui" },
			files = {},
		}),
		[base .. "packages/trapos-core/ccpm.json"] = textutils.serializeJSON({
			name = "trapos-core",
			version = "1",
			dependencies = {},
			files = { "programs/ccpm.lua" },
		}),
		[base .. "packages/trapos-ui/ccpm.json"] = textutils.serializeJSON({
			name = "trapos-ui",
			version = "1",
			dependencies = { "trapos-core" },
			files = { "apis/libtui.lua" },
			autostart = { "daemons/demo" },
		}),
		[base .. "packages/trapos-core/programs/ccpm.lua"] = "ccpm-body",
		[base .. "packages/trapos-ui/apis/libtui.lua"] = "ui-body",
	}
	local sd, root = freshDirs()
	local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) })
	ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } })

	testlib.assertTrue(ccpm.install("trapos", {}))

	local f = fs.open(sd .. "/manifest.json", "r")
	local manifest = textutils.unserializeJSON(f.readAll())
	f.close()
	testlib.assertEquals(manifest.version, "1")
	testlib.assertEquals(#manifest.files, 2)
	testlib.assertEquals(manifest.autostart[1], "daemons/demo")
end)

testlib.test("registry add and remove round-trip", function()
	local ccpm = createCcpm({ stateDir = freshDirs() })
	ccpm.writeConfig({ registries = {} })
	testlib.assertTrue(ccpm.addRegistry("foo/bar", { branch = "next" }))

	local regs = ccpm.listRegistries()
	testlib.assertEquals(#regs, 1)
	testlib.assertEquals(regs[1].name, "foo/bar")
	testlib.assertEquals(regs[1].branch, "next")

	local dupOk = ccpm.addRegistry("foo/bar", {})
	testlib.assertTrue(not dupOk)

	testlib.assertTrue(ccpm.removeRegistry("foo/bar"))
	testlib.assertEquals(#ccpm.listRegistries(), 0)

	local rmOk = ccpm.removeRegistry("nope")
	testlib.assertTrue(not rmOk)
end)

testlib.test("addRegistry defaults to a gitea registry", function()
	local ccpm = createCcpm({ stateDir = freshDirs() })
	ccpm.writeConfig({ registries = {} })

	local ok, registry = ccpm.addRegistry("foo/bar", {})
	testlib.assertTrue(ok)
	testlib.assertEquals(registry.type, "gitea")
	testlib.assertEquals(registry.branch, "master")

	local regs = ccpm.listRegistries()
	testlib.assertEquals(regs[1].type, "gitea")
	testlib.assertEquals(ccpm.registryBaseUrl(regs[1]), "https://git.trapcloud.fr/foo/bar/raw/branch/master/")

	-- An explicit type is still honored.
	testlib.assertTrue(ccpm.addRegistry("http://example.com/repo", { type = "http" }))
	local explicit = ccpm.listRegistries()[2]
	testlib.assertEquals(explicit.type, "http")
end)

testlib.test("compareVersions treats padded zeros as equal", function()
	-- compareVersions is internal; probe via available() status
	local ccpm = createCcpm({ stateDir = freshDirs() })
	ccpm.writeCache({ packages = { pkg = { version = "1.0.0", registry = "r" } } })
	ccpm.writeLock({ packages = { pkg = { version = "1.0", registry = "r", files = {}, dependencies = {} } } })
	local avail = ccpm.available()
	testlib.assertEquals(avail[1].status, "up-to-date")
end)

testlib.test("update writes a package cache from registries", function()
	local base = giteaBase("me/repo", "master")
	local routes = {
		[base .. "packages/index.json"] = textutils.serializeJSON({ packages = { foo = "1.0.0", bar = "2.0.0" } }),
	}
	local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) })
	ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } })

	local cache = ccpm.update()
	testlib.assertEquals(cache.packages.foo.version, "1.0.0")
	testlib.assertEquals(cache.packages.foo.registry, "me/repo")

	local search = ccpm.search("ba")
	testlib.assertEquals(#search, 1)
	testlib.assertEquals(search[1].name, "bar")
end)

testlib.test("available marks cached packages by install status", function()
	local ccpm = createCcpm({ stateDir = freshDirs() })
	ccpm.writeCache({
		packages = {
			alpha = { version = "1.0.0", registry = "me/repo" },
			beta = { version = "2.0.0", registry = "me/repo" },
			gamma = { version = "1.0.0", registry = "me/repo" },
		},
	})
	ccpm.writeLock({
		packages = {
			alpha = { version = "1.0.0", files = {}, dependencies = {} },
			beta = { version = "1.0.0", files = {}, dependencies = {} },
		},
	})

	local available = ccpm.available()
	testlib.assertEquals(#available, 3)
	testlib.assertEquals(available[1].name, "alpha")
	testlib.assertEquals(available[1].status, "up-to-date")
	testlib.assertEquals(available[2].name, "beta")
	testlib.assertEquals(available[2].status, "updatable")
	testlib.assertEquals(available[3].name, "gamma")
	testlib.assertEquals(available[3].status, "available")
end)

testlib.test("upgrade reinstalls outdated packages from cache", function()
	local base = giteaBase("me/repo", "master")
	local routes = {
		[base .. "packages/foo/ccpm.json"] = textutils.serializeJSON({
			name = "foo",
			version = "2.0.0",
			dependencies = {},
			files = { "programs/foo.lua" },
		}),
		[base .. "packages/foo/programs/foo.lua"] = "foo-v2",
	}
	local sd, root = freshDirs()
	local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) })
	ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } })
	ccpm.writeCache({ packages = { foo = { version = "2.0.0", registry = "me/repo" } } })
	ccpm.writeLock({
		packages = {
			foo = { version = "1.0.0", registry = "me/repo", files = { "programs/foo.lua" }, dependencies = {} },
		},
	})

	local ok, upgraded = ccpm.upgrade({})
	testlib.assertTrue(ok)
	testlib.assertEquals(#upgraded, 1)
	testlib.assertEquals(upgraded[1], "foo")
	testlib.assertEquals(ccpm.readLock().packages.foo.version, "2.0.0")

	local f = fs.open(root .. "/programs/foo.lua", "r")
	local body = f.readAll()
	f.close()
	testlib.assertEquals(body, "foo-v2")
end)

testlib.test("upgrade requires a package cache", function()
	local ccpm = createCcpm({ stateDir = freshDirs() })
	ccpm.writeLock({ packages = { foo = { version = "1.0.0", files = {}, dependencies = {} } } })

	local ok, err = ccpm.upgrade({})
	testlib.assertTrue(not ok)
	testlib.assertTrue(string.find(err, "ccpm update", 1, true))
end)

testlib.test("prune removes orphans and keeps explicit roots", function()
  local sd, root = freshDirs();
  writeFile(root .. "/programs/app.lua", "app");
  writeFile(root .. "/apis/dep.lua", "dep");
  writeFile(root .. "/programs/orphan.lua", "orphan");
  writeFile(root .. "/apis/orphan-dep.lua", "orphan-dep");

  local ccpm = createCcpm({ stateDir = sd, installRoot = root });
  ccpm.writeLock({
    packages = {
      app = { version = "1", files = { "programs/app.lua" }, dependencies = { "dep" }, explicit = true },
      dep = { version = "1", files = { "apis/dep.lua" }, dependencies = {}, explicit = false },
      orphan = { version = "1", files = { "programs/orphan.lua" }, dependencies = { "orphan-dep" }, explicit = false },
      ["orphan-dep"] = { version = "1", files = { "apis/orphan-dep.lua" }, dependencies = {}, explicit = false },
    },
  });

  local preview = ccpm.prune({ dryRun = true });
  testlib.assertEquals(#preview, 2);
  testlib.assertEquals(preview[1], "orphan");
  testlib.assertEquals(preview[2], "orphan-dep");
  testlib.assertTrue(fs.exists(root .. "/programs/orphan.lua"));

  local removed = ccpm.prune({});
  testlib.assertEquals(#removed, 2);
  testlib.assertTrue(ccpm.readLock().packages.app ~= nil);
  testlib.assertTrue(ccpm.readLock().packages.dep ~= nil);
  testlib.assertTrue(ccpm.readLock().packages.orphan == nil);
  testlib.assertTrue(not fs.exists(root .. "/programs/orphan.lua"));
  testlib.assertTrue(not fs.exists(root .. "/apis/orphan-dep.lua"));
  testlib.assertTrue(fs.exists(root .. "/programs/app.lua"));
  testlib.assertTrue(fs.exists(root .. "/apis/dep.lua"));
end)

testlib.test("uninstall autoprunes orphaned dependencies", function()
  local sd, root = freshDirs();
  writeFile(root .. "/apis/core.lua", "core");
  writeFile(root .. "/programs/feature.lua", "feature");

  local ccpm = createCcpm({ stateDir = sd, installRoot = root });
  ccpm.writeLock({
    packages = {
      trapos = { version = "1", files = {}, dependencies = { "feature" }, explicit = true },
      feature = { version = "1", files = { "programs/feature.lua" }, dependencies = { "core" }, explicit = false },
      core = { version = "1", files = { "apis/core.lua" }, dependencies = {}, explicit = false },
    },
  });

  local ok, removed = ccpm.uninstall("trapos", {});
  testlib.assertTrue(ok);
  testlib.assertEquals(#removed, 3);
  testlib.assertEquals(removed[1], "trapos");
  testlib.assertEquals(removed[2], "core");
  testlib.assertEquals(removed[3], "feature");
  testlib.assertTrue(not fs.exists(root .. "/apis/core.lua"));
  testlib.assertTrue(not fs.exists(root .. "/programs/feature.lua"));
  testlib.assertTrue(not fs.exists(sd));
end)

testlib.test("uninstall can skip pruning", function()
  local sd, root = freshDirs();
  writeFile(root .. "/apis/core.lua", "core");

  local ccpm = createCcpm({ stateDir = sd, installRoot = root });
  ccpm.writeLock({
    packages = {
      trapos = { version = "1", files = {}, dependencies = { "core" }, explicit = true },
      core = { version = "1", files = { "apis/core.lua" }, dependencies = {}, explicit = false },
    },
  });

  local ok, removed = ccpm.uninstall("trapos", { prune = false });
  testlib.assertTrue(ok);
  testlib.assertEquals(#removed, 1);
  testlib.assertTrue(ccpm.readLock().packages.trapos == nil);
  testlib.assertTrue(ccpm.readLock().packages.core ~= nil);
  testlib.assertTrue(fs.exists(root .. "/apis/core.lua"));
  testlib.assertTrue(fs.exists(sd));
end)

testlib.test("uninstalling the last package deletes state and empty generated dirs", function()
  local sd, root = freshDirs();
  writeFile(root .. "/programs/solo.lua", "solo");

  local ccpm = createCcpm({ stateDir = sd, installRoot = root });
  ccpm.writeLock({
    packages = {
      solo = { version = "1", files = { "programs/solo.lua" }, dependencies = {}, explicit = true },
    },
  });

  testlib.assertTrue(ccpm.uninstall("solo", {}));
  testlib.assertTrue(not fs.exists(root .. "/programs/solo.lua"));
  testlib.assertTrue(not fs.exists(root .. "/programs"));
  testlib.assertTrue(not fs.exists(sd));
end)

testlib.test("legacy entries without explicit are not pruned", function()
  local ccpm = createCcpm({ stateDir = freshDirs() });
  ccpm.writeLock({
    packages = {
      legacy = { version = "1", files = {}, dependencies = { "dep" } },
      dep = { version = "1", files = {}, dependencies = {}, explicit = false },
      orphan = { version = "1", files = {}, dependencies = {}, explicit = false },
    },
  });

  local removed = ccpm.prune({});
  testlib.assertEquals(#removed, 1);
  testlib.assertEquals(removed[1], "orphan");
  testlib.assertTrue(ccpm.readLock().packages.legacy ~= nil);
  testlib.assertTrue(ccpm.readLock().packages.dep ~= nil);
end)

testlib.test("planUninstall previews autoprune removals", function()
  local ccpm = createCcpm({ stateDir = freshDirs() });
  ccpm.writeLock({
    packages = {
      trapos = { version = "1", files = {}, dependencies = { "core" }, explicit = true },
      core = { version = "1", files = {}, dependencies = {}, explicit = false },
    },
  });

  local ok, planned = ccpm.planUninstall("trapos", {});
  testlib.assertTrue(ok);
  testlib.assertEquals(#planned, 2);
  testlib.assertEquals(planned[1], "trapos");
  testlib.assertEquals(planned[2], "core");
end)

testlib.test("uninstall refuses a package with dependents", function()
	local ccpm = createCcpm({ stateDir = freshDirs() })
	ccpm.writeLock({
		packages = {
			["trapos-core"] = { version = "1", files = {}, dependencies = {} },
			["trapos-ui"] = { version = "1", files = {}, dependencies = { "trapos-core" } },
		},
	})
	local ok, err = ccpm.uninstall("trapos-core", {})
	testlib.assertTrue(not ok)
	testlib.assertTrue(string.find(err, "required by", 1, true))
	testlib.assertTrue(string.find(err, "trapos-ui", 1, true))
end)

testlib.test("uninstall removes a leaf package and its files", function()
	local sd, root = freshDirs()
	fs.makeDir(root .. "/apis")
	local f = fs.open(root .. "/apis/libtui.lua", "w")
	f.write("x")
	f.close()

	local ccpm = createCcpm({ stateDir = sd, installRoot = root })
	ccpm.writeLock({
		packages = {
			["trapos-core"] = { version = "1", files = {}, dependencies = {} },
			["trapos-ui"] = { version = "1", files = { "apis/libtui.lua" }, dependencies = { "trapos-core" } },
		},
	})

	testlib.assertTrue(ccpm.uninstall("trapos-ui", {}))
	testlib.assertTrue(not fs.exists(root .. "/apis/libtui.lua"))
	testlib.assertTrue(ccpm.readLock().packages["trapos-ui"] == nil)
	testlib.assertTrue(ccpm.readLock().packages["trapos-core"] ~= nil)
end)

testlib.run()
