const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const {
  pruneEntries,
  replacePluginDirectory,
  resetDirectoryForFullUpdate
} = require("../src/services/update-manager");

const root = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-plugin-update-test-"));
try {
  const source = path.join(root, "source");
  const target = path.join(root, "target");
  fs.mkdirSync(path.join(source, "data", "config"), { recursive: true });
  fs.mkdirSync(path.join(target, "data", "models"), { recursive: true });
  fs.writeFileSync(path.join(source, "plugin.json"), '{"id":"test"}');
  fs.writeFileSync(path.join(source, "index.js"), "module.exports = 'new';");
  fs.writeFileSync(path.join(source, "data", "config", "default.json"), '{"default":true}');
  fs.writeFileSync(path.join(target, "index.js"), "module.exports = 'old';");
  fs.writeFileSync(path.join(target, "stale.js"), "stale");
  const model = path.join(target, "data", "models", "large.gguf");
  const descriptor = fs.openSync(model, "w");
  fs.ftruncateSync(descriptor, 3 * 1024 * 1024 * 1024);
  fs.closeSync(descriptor);

  const originalRenameForLockedRoot = fs.renameSync;
  fs.renameSync = (from, to) => {
    if (from === target) {
      throw Object.assign(new Error("simulated running plugin data lock"), { code: "EPERM" });
    }
    return originalRenameForLockedRoot(from, to);
  };
  try {
    replacePluginDirectory(source, target, { preserveData: true });
  } finally {
    fs.renameSync = originalRenameForLockedRoot;
  }

  assert.equal(fs.readFileSync(path.join(target, "index.js"), "utf8"), "module.exports = 'new';");
  assert.equal(fs.existsSync(path.join(target, "stale.js")), false);
  assert.equal(fs.statSync(model).size, 3 * 1024 * 1024 * 1024);
  assert.equal(fs.existsSync(path.join(target, "data", "config", "default.json")), false);

  const failedSource = path.join(root, "failed-source");
  fs.mkdirSync(failedSource, { recursive: true });
  fs.writeFileSync(path.join(failedSource, "plugin.json"), '{"id":"test"}');
  fs.writeFileSync(path.join(failedSource, "index.js"), "module.exports = 'must-not-land';");
  const originalRename = fs.renameSync;
  fs.renameSync = (from, to) => {
    if (String(from).includes(".target.update-") && path.basename(from) === "index.js") {
      throw Object.assign(new Error("simulated install lock"), { code: "EPERM" });
    }
    return originalRename(from, to);
  };
  try {
    assert.throws(
      () => replacePluginDirectory(failedSource, target, { preserveData: true }),
      /simulated install lock/
    );
  } finally {
    fs.renameSync = originalRename;
  }
  assert.equal(fs.readFileSync(path.join(target, "index.js"), "utf8"), "module.exports = 'new';");
  assert.equal(fs.statSync(model).size, 3 * 1024 * 1024 * 1024);
  assert.equal(fs.readdirSync(root).some((name) => name.includes(".target.update-") || name.includes(".target.backup-")), false);

  const install = path.join(root, "install");
  for (const relative of [
    "data/feedback/report.json",
    "data/lumi_ai/models/model.gguf",
    "plugins/example/data/settings.json",
    "knowledge/community/local.md",
    "knowledge/corrections/fix.md",
    "knowledge/core/generated.md",
    "knowledge/core/local.md",
    "knowledge/plugins/generated.md",
    "config/local.json",
    "src/stale.js"
  ]) {
    const file = path.join(install, relative);
    fs.mkdirSync(path.dirname(file), { recursive: true });
    fs.writeFileSync(file, relative);
  }
  fs.writeFileSync(
    path.join(install, "knowledge", "core", "generated.md"),
    "---\ngenerated: true\neditable: false\n---\ngenerated"
  );
  fs.writeFileSync(
    path.join(install, "knowledge", "plugins", "generated.md"),
    "---\ngenerated: true\neditable: false\n---\ngenerated"
  );
  fs.writeFileSync(
    path.join(install, "knowledge", "core", "local.md"),
    "---\ngenerated: false\neditable: true\n---\nlocal"
  );
  resetDirectoryForFullUpdate(install);
  for (const relative of [
    "data/feedback/report.json",
    "data/lumi_ai/models/model.gguf",
    "plugins/example/data/settings.json",
    "knowledge/community/local.md",
    "knowledge/corrections/fix.md",
    "knowledge/core/local.md",
    "config/local.json"
  ]) {
    assert.equal(fs.existsSync(path.join(install, relative)), true, `${relative} should be preserved`);
  }
  assert.equal(fs.existsSync(path.join(install, "knowledge/core/generated.md")), false);
  assert.equal(fs.existsSync(path.join(install, "knowledge/plugins/generated.md")), false);
  assert.equal(fs.existsSync(path.join(install, "src/stale.js")), false);

  const snapshotRoot = path.join(root, "snapshots");
  const now = Date.now();
  const makeSnapshot = (id, createdAt, type = "bot", pluginId = null) => {
    const snapshotPath = path.join(snapshotRoot, id);
    fs.mkdirSync(snapshotPath, { recursive: true });
    fs.writeFileSync(path.join(snapshotPath, type === "bot" ? "core.zip" : "plugin.zip"), "compressed-code");
    return {
      id,
      type,
      pluginId,
      target_kind: type,
      target_id: pluginId,
      status: "available",
      createdAt,
      path: snapshotPath
    };
  };
  const newestCore = makeSnapshot("core-new", now);
  const secondCore = makeSnapshot("core-second", now - 1000);
  const excessCore = makeSnapshot("core-excess", now - 2000);
  const pluginSnapshot = makeSnapshot("plugin-new", now - 3000, "plugin", "example");
  const stalePlugin = makeSnapshot("plugin-stale", now - 31 * 24 * 60 * 60 * 1000, "plugin", "old");
  fs.mkdirSync(path.join(newestCore.path, "full", "data", "lumi_ai", "models"), { recursive: true });
  fs.writeFileSync(path.join(newestCore.path, "full", "data", "lumi_ai", "models", "model.gguf"), "must-not-remain");
  fs.writeFileSync(path.join(newestCore.path, "app.db"), "database-backup");
  const retained = pruneEntries(
    [newestCore, secondCore, excessCore, pluginSnapshot, stalePlugin],
    { now, retention: { max_age_days: 30, max_per_target: 2 } }
  );
  assert.deepEqual(retained.map((entry) => entry.id).sort(), ["core-new", "core-second", "plugin-new"]);
  assert.equal(fs.existsSync(path.join(newestCore.path, "full")), false);
  assert.equal(fs.existsSync(path.join(newestCore.path, "app.db")), false);
  assert.equal(fs.existsSync(path.join(newestCore.path, "database.zip")), true);
  assert.equal(fs.existsSync(excessCore.path), false);
  assert.equal(fs.existsSync(stalePlugin.path), false);
  assert.equal(fs.existsSync(pluginSnapshot.path), true);

  const managerSource = fs.readFileSync(path.join(__dirname, "..", "src", "services", "update-manager.js"), "utf8");
  const updateDocs = fs.readFileSync(path.join(__dirname, "..", "docs", "updates.md"), "utf8");
  const updateView = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "admin-updates.ejs"), "utf8");
  const mainSource = fs.readFileSync(path.join(__dirname, "..", "src", "main.js"), "utf8");
  assert(managerSource.includes("Automatic rollback also failed"));
  assert.equal((managerSource.match(/restoreSnapshot\(record\.id/g) || []).length >= 2, true);
  assert.equal(managerSource.includes("snapshotFullInstall(fullPath)"), false);
  assert(managerSource.includes("database.zip"));
  assert(managerSource.includes("DEFAULT_SNAPSHOT_RETENTION_DAYS = 30"));
  assert(updateView.includes("Automatic snapshot cleanup"));
  assert(updateView.includes('name="max_age_days"'));
  assert(updateView.includes('name="max_per_target"'));
  assert(mainSource.includes("cleanupSnapshots();"));
  assert(updateDocs.includes("Preserved Local Data"));
  assert(updateDocs.includes("knowledge/community/"));
  assert(updateDocs.includes("AI models/runtimes"));
  assert(updateDocs.includes("LLM model files"));
  console.log("Update preservation verification passed: protected data, compressed snapshots, retention cleanup, transactional replacement, and failed-update rollback wiring.");
} finally {
  fs.rmSync(root, { recursive: true, force: true });
}
