const assert = require("assert");
const fs = require("fs");
const path = require("path");

const root = path.join(__dirname, "..");
const sandbox = fs.mkdtempSync(path.join(root, ".tmp-lumi-overlays-"));
const serviceDir = path.join(sandbox, "src", "services");
fs.mkdirSync(serviceDir, { recursive: true });

for (const file of [
  "config.js",
  "db.js",
  "settings.js",
  "web-events.js",
  "overlay-secrets.js",
  "overlay-modules.js",
  "overlays.js",
  "overlay-permissions.js",
  "overlay-connectors.js"
]) {
  fs.copyFileSync(path.join(root, "src", "services", file), path.join(serviceDir, file));
}

let database = null;
try {
  database = require(path.join(serviceDir, "db.js"));
  database.migrate();
  const settings = require(path.join(serviceDir, "settings.js"));
  settings.ensureDefaults();
  settings.setSetting("session_secret", "overlay-verification-session-secret");
  const overlays = require(path.join(serviceDir, "overlays.js"));
  const overlayModules = require(path.join(serviceDir, "overlay-modules.js"));
  const permissions = require(path.join(serviceDir, "overlay-permissions.js"));
  const connectors = require(path.join(serviceDir, "overlay-connectors.js"));
  const providerList = connectors.listOverlayConnectorProviders();
  assert(providerList.some((provider) => provider.id === "local_obs_websocket" && provider.available !== false));
  assert(providerList.some((provider) => provider.id === "obs_browser_bridge" && provider.available !== false));
  assert(providerList.some((provider) => provider.id === "trusted_lumi_client" && provider.available === false));
  const bridge = new connectors.ObsBrowserBridgeConnector({ overlay_id: "bridge-test", sync_direction: "obs_to_overlay" });
  const bridgeReport = bridge.acceptReport({
    instance_id: "obs-browser-test",
    plugin_version: "2.24.0",
    control_level: 2,
    current_scene: "Starting Soon",
    scenes: ["Starting Soon", "Live"],
    transitions: ["Cut", "Fade"],
    current_transition: "Fade",
    canvas_width: 1920,
    canvas_height: 1080,
    outputs: { streaming: true },
    source_active: true,
    source_visible: true
  });
  assert.strictEqual(bridgeReport.accepted, true);
  assert.strictEqual(bridgeReport.current_scene, "Starting Soon");
  assert.deepStrictEqual(bridgeReport.scenes, ["Starting Soon", "Live"]);
  assert.strictEqual(bridgeReport.control_level_label, "Read scenes and transitions");
  assert.strictEqual(bridgeReport.profiles_supported, false);
  bridge.disconnect();
  const sourceTypes = overlayModules.listOverlayModuleTypes();
  assert(sourceTypes.some((type) => type.id === "video"));
  assert(sourceTypes.some((type) => type.id === "audio"));

  const created = overlays.createOverlay({ name: "Main Stream", sceneName: "Live" });
  assert.strictEqual(created.scenes.length, 1, "new overlays must receive a first scene");
  assert.strictEqual(created.canvas_width, 1920);
  assert.strictEqual(created.canvas_height, 1080);
  assert(created.public_token.length >= 40, "overlay token must be high entropy");
  assert(created.scenes[0].public_token.length >= 40, "scene token must be high entropy");

  const resolved = overlays.resolvePublicOverlay(created.public_token);
  assert.strictEqual(resolved.overlay.id, created.id);
  const fixed = overlays.resolvePublicOverlay(created.public_token, created.scenes[0].public_token);
  assert.strictEqual(fixed.fixedScene.id, created.scenes[0].id);

  const headlineId = overlays.addModule(created.scenes[0].id, {
    name: "Headline",
    type: "text",
    enabled: true,
    config: { text: "Starting soon", x: 5, y: 10, width: 90, height: 20 }
  });
  const state = overlays.buildPublicState(created.id);
  assert.strictEqual(state.scene.modules.length, 1);
  assert.strictEqual(state.scene.modules[0].config.text, "Starting soon");
  overlays.updateModuleTransform(headlineId, { x: 50, y: 50, width: 40, height: 15, anchor: "center" });
  const transformed = overlays.getOverlay(created.id).scenes[0].modules[0];
  assert.strictEqual(transformed.config.anchor, "center");
  assert.strictEqual(transformed.config.text, "Starting soon", "canvas transforms must preserve source content");

  const textConfig = overlayModules.normalizeModuleConfig("text", {
    horizontalAlign: "right",
    verticalAlign: "bottom",
    overflow: "shrink",
    anchor: "bottom-right"
  });
  assert.strictEqual(textConfig.horizontalAlign, "right");
  assert.strictEqual(textConfig.verticalAlign, "bottom");
  assert.strictEqual(textConfig.overflow, "shrink");
  assert.strictEqual(textConfig.anchor, "bottom-right");
  for (const supportedColor of ["#fff", "#ffff", "#ffffff", "#ffffffff"]) {
    assert.strictEqual(overlayModules.normalizeModuleConfig("text", { color: supportedColor }).color, supportedColor);
  }
  assert.strictEqual(overlayModules.normalizeModuleConfig("text", { color: "#fffff" }).color, "#ffffff");
  const webConfig = overlayModules.normalizeModuleConfig("web", {
    url: "https://example.com/alerts",
    autoRefresh: true,
    healthCheck: true,
    refreshIntervalSeconds: 10,
    retryLimit: 9
  });
  assert.strictEqual(webConfig.autoRefresh, true);
  assert.strictEqual(webConfig.healthCheck, true);
  assert.strictEqual(webConfig.refreshIntervalSeconds, 30);
  assert.strictEqual(webConfig.retryLimit, 5);
  const videoConfig = overlayModules.normalizeModuleConfig("video", {
    url: "https://example.com/alert.webm",
    playBehavior: "loop",
    muted: false,
    volume: 0.65,
    playbackRate: 1.25,
    startAt: 2.5,
    fit: "cover"
  });
  assert.strictEqual(videoConfig.playBehavior, "loop");
  assert.strictEqual(videoConfig.volume, 0.65);
  assert.strictEqual(videoConfig.playbackRate, 1.25);
  assert.strictEqual(videoConfig.startAt, 2.5);
  assert.strictEqual(videoConfig.fit, "cover");
  const audioConfig = overlayModules.normalizeModuleConfig("audio", {
    url: "https://example.com/alert.ogg",
    playBehavior: "manual",
    volume: 5
  });
  assert.strictEqual(audioConfig.playBehavior, "manual");
  assert.strictEqual(audioConfig.showControls, true);
  assert.strictEqual(audioConfig.volume, 1);
  const styledWebConfig = overlayModules.normalizeModuleConfig("web", {
    url: "https://example.com/widget",
    cropLeft: 12.5,
    zoom: 1.4,
    customCss: "iframe { filter: grayscale(1); }"
  });
  assert.strictEqual(styledWebConfig.cropLeft, 12.5);
  assert.strictEqual(styledWebConfig.zoom, 1.4);
  assert.strictEqual(styledWebConfig.customCss, "iframe { filter: grayscale(1); }");

  overlays.updateOverlay(created.id, { canvasWidth: 1280, canvasHeight: 720 });
  assert.deepStrictEqual(overlays.buildPublicState(created.id).canvas, { width: 1280, height: 720 });

  const second = overlays.addScene(created.id, { name: "Break", obsSceneName: "BRB" });
  overlays.setActiveScene(created.id, second.id, { source: "verify" });
  assert.strictEqual(overlays.buildPublicState(created.id).scene.id, second.id);
  assert.strictEqual(overlays.buildPublicState(created.id, created.scenes[0].id).scene.name, "Live");

  const oldOverlayToken = created.public_token;
  const newOverlayToken = overlays.regenerateOverlayToken(created.id);
  assert.strictEqual(overlays.resolvePublicOverlay(oldOverlayToken), null, "regeneration must revoke old overlay tokens");
  assert(overlays.resolvePublicOverlay(newOverlayToken));

  const refreshed = overlays.getOverlay(created.id, { includeSecrets: true });
  const firstScene = refreshed.scenes.find((scene) => scene.name === "Live");
  const oldSceneToken = firstScene.public_token;
  const newSceneToken = overlays.regenerateSceneToken(firstScene.id);
  assert.strictEqual(overlays.resolvePublicOverlay(newOverlayToken, oldSceneToken), null, "regeneration must revoke old fixed scene tokens");
  assert(overlays.resolvePublicOverlay(newOverlayToken, newSceneToken));

  overlays.deleteScene(second.id);
  assert.throws(() => overlays.deleteScene(firstScene.id), /at least one scene/);

  const duplicate = overlays.duplicateOverlay(created.id);
  assert.notStrictEqual(duplicate.public_token, newOverlayToken);
  assert.strictEqual(duplicate.scenes.length, 1);
  assert.strictEqual(duplicate.scenes[0].modules.length, 1);

  overlays.saveObsSettings(created.id, {
    enabled: false,
    provider: "local_obs_websocket",
    endpoint: "ws://127.0.0.1:4455",
    password: "verification-secret",
    syncDirection: "bidirectional",
    reconnect: true
  });
  const storedObs = database.db.prepare("SELECT password_encrypted FROM overlay_obs_settings WHERE overlay_id = ?").get(created.id);
  assert(storedObs.password_encrypted && !storedObs.password_encrypted.includes("verification-secret"));
  assert.strictEqual(overlays.getObsSettings(created.id, { includeSecret: true }).password, "verification-secret");

  assert.strictEqual(permissions.canAccessOverlay({ isAdmin: true }, created.id, "view_secrets"), true);
  assert.strictEqual(permissions.canAccessOverlay({ isMod: true }, created.id, "view_secrets"), false);
  const unregister = permissions.registerOverlayAccessProvider({
    allows: ({ overlayId, capability }) => overlayId === created.id && capability === "manage"
  });
  assert.strictEqual(permissions.canAccessOverlay({ isMod: true }, created.id, "manage"), true);
  assert.strictEqual(permissions.canAccessOverlay({ isMod: true }, created.id, "view_secrets"), false);
  unregister();

  const editorView = fs.readFileSync(path.join(root, "src", "web", "views", "admin-overlay-detail.ejs"), "utf8");
  const editorScript = fs.readFileSync(path.join(root, "src", "web", "public", "overlay-management.js"), "utf8");
  const runtimeScript = fs.readFileSync(path.join(root, "src", "web", "public", "overlay-runtime.js"), "utf8");
  const rendererScript = fs.readFileSync(path.join(root, "src", "web", "public", "overlay-renderer.js"), "utf8");
  const connectorScript = fs.readFileSync(path.join(root, "src", "services", "overlay-connectors.js"), "utf8");
  const routeScript = fs.readFileSync(path.join(root, "src", "services", "overlay-routes.js"), "utf8");
  assert(editorView.includes("data-overlay-canvas"), "management page must include a visual canvas");
  assert(editorView.includes('data-module-fields="text"') && editorView.includes('data-module-fields="web"'));
  assert(editorView.includes('data-module-fields="video"') && editorView.includes('data-module-fields="audio"'));
  assert(editorView.includes('name="custom_css"') && editorView.includes('name="crop_left"'));
  assert(editorView.includes("data-color-picker") && editorView.includes("overlay-transform-disclosure"));
  assert(editorView.includes('data-confirm-action="/admin/overlays/<%= overlay.id %>/modules/<%= module.id %>/delete"'), "source deletion must use the shared timed-confirmation action flow");
  assert(editorScript.includes("data-resize-handle") && editorScript.includes("snapTargets"));
  assert(editorScript.includes("--overlay-settings-max-height"), "settings height must follow its viewport position");
  assert(runtimeScript.includes("overlay:module-refresh") && runtimeScript.includes("checkWebsiteSource"));
  assert(runtimeScript.includes("window.obsstudio") && runtimeScript.includes("overlay:obs-browser-command"));
  assert(rendererScript.includes("buildMedia") && rendererScript.includes("buildWebsite"));
  assert(rendererScript.includes('frame.setAttribute("allow", "autoplay; fullscreen")'));
  assert(connectorScript.includes('id: "obs_browser_bridge"') && connectorScript.includes("acceptReport(report"));
  assert(routeScript.includes("/obs-bridge") && routeScript.includes("/obs/import"));

  console.log("Overlay verification passed: CRUD, visual editor hooks, transforms, layout controls, external recovery, token revocation, encrypted OBS credentials, and permissions.");
} finally {
  database?.db.close();
  fs.rmSync(sandbox, { recursive: true, force: true });
}
