const assert = require("node:assert/strict");
const { readFile } = require("node:fs/promises");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");

const MealSnapshotService = require("../js/meal-snapshot-service.js");
const MealSnapshotUiState = require("../js/meal-snapshot-ui-state.js");
const MealSnapshotUiRenderer = require("../js/meal-snapshot-ui-renderer.js");
const MealSnapshotMockEstimate = require("../js/meal-snapshot-mock-estimate.js");
const MealSnapshotReviewRenderer = require("../js/meal-snapshot-review-renderer.js");
const MealSnapshotReviewedDraft = require("../js/meal-snapshot-reviewed-draft.js");
const MealSnapshotFallbackRenderer = require("../js/meal-snapshot-fallback-renderer.js");
const { mountMealSnapshotBetaController } = require("../js/meal-snapshot-ui-controller.js");

class FakeElement {
  constructor(tagName, ownerDocument) {
    this.tagName = String(tagName).toUpperCase();
    this.ownerDocument = ownerDocument;
    this.children = [];
    this.attributes = {};
    this.listeners = {};
    this.className = "";
    this._textContent = "";
    this.type = "";
    this.value = "";
    this.files = [];
  }
  set textContent(value) { this._textContent = String(value); this.children = []; }
  get textContent() { return this._textContent + this.children.map((child) => child.textContent).join(""); }
  set innerHTML(value) { throw new Error(`Unsafe HTML assignment: ${value}`); }
  append(...nodes) { this.children.push(...nodes); }
  replaceChildren(...nodes) { this._textContent = ""; this.children = [...nodes]; }
  setAttribute(name, value) { this.attributes[name] = String(value); }
  addEventListener(type, listener) { this.listeners[type] = listener; }
  removeEventListener(type, listener) { if (this.listeners[type] === listener) delete this.listeners[type]; }
  dispatch(type) { this.listeners[type]?.({ currentTarget: this }); }
  click() { this.dispatch("click"); }
}

class FakeDocument {
  createElement(tagName) { return new FakeElement(tagName, this); }
}

function descendants(node) {
  return node.children.flatMap((child) => [child, ...descendants(child)]);
}

function byAttribute(container, name, value) {
  return descendants(container).filter((node) => node.attributes[name] === value);
}

function mounted() {
  const documentRef = new FakeDocument();
  const container = documentRef.createElement("div");
  const events = [];
  const controller = mountMealSnapshotBetaController({
    container,
    featureConfig: { enabled: true, allowConfirmedSave: false },
    uiStateApi: MealSnapshotUiState,
    rendererApi: MealSnapshotUiRenderer,
    mockEstimateApi: MealSnapshotMockEstimate,
    reviewRendererApi: MealSnapshotReviewRenderer,
    reviewedDraftApi: MealSnapshotReviewedDraft,
    fallbackRendererApi: MealSnapshotFallbackRenderer,
    validateFile: MealSnapshotService.validateFile,
    maxImageBytes: MealSnapshotService.DEFAULT_MAX_IMAGE_BYTES,
    onEvent: (event) => events.push(event),
    documentRef
  });
  return { container, controller, events };
}

test("manual fallback fields remain controller-local, bounded, and absent from events", () => {
  const { container, controller, events } = mounted();
  byAttribute(container, "data-meal-snapshot-action", "manual_entry")[0].click();
  const title = byAttribute(container, "data-fallback-field", "title")[0];
  const notes = byAttribute(container, "data-fallback-field", "notes")[0];
  const calories = byAttribute(container, "data-fallback-field", "calories")[0];
  title.value = "T".repeat(140);
  title.dispatch("input");
  notes.value = "private portion note";
  notes.dispatch("input");
  calories.value = "450";
  calories.dispatch("input");

  assert.equal(title.attributes.maxlength, "100");
  assert.equal(notes.attributes.maxlength, "1000");
  assert.equal(calories.attributes.min, "0");
  assert.equal(calories.attributes.max, "10000");
  assert.equal(JSON.stringify(events).includes("private portion note"), false);
  assert.deepEqual(Object.keys(events.at(-1)).sort(), [
    "canManualEntry", "canNoteOnly", "canRetry", "canSaveConfirmed", "event", "status"
  ]);
});

test("invalid numeric edits are ignored and clear restores the empty manual draft", () => {
  const { container, controller } = mounted();
  byAttribute(container, "data-meal-snapshot-action", "manual_entry")[0].click();
  const protein = byAttribute(container, "data-fallback-field", "protein")[0];
  protein.value = "42";
  protein.dispatch("input");
  byAttribute(container, "data-fallback-action", "reset")[0].click();
  assert.equal(byAttribute(container, "data-fallback-field", "title")[0].value, "");
  assert.equal(byAttribute(container, "data-fallback-field", "protein")[0].value, "");
});

test("note-only draft is local, resettable, and discarded on back", () => {
  const { container, controller, events } = mounted();
  byAttribute(container, "data-meal-snapshot-action", "save_note_only")[0].click();
  const note = byAttribute(container, "data-fallback-field", "note")[0];
  note.value = "N".repeat(1200);
  note.dispatch("input");
  assert.equal(note.attributes.maxlength, "1000");
  assert.equal(JSON.stringify(events).includes("NNNN"), false);
  byAttribute(container, "data-fallback-action", "reset")[0].click();
  assert.equal(byAttribute(container, "data-fallback-field", "note")[0].value, "");
  byAttribute(container, "data-fallback-action", "back")[0].click();
  assert.equal(controller.getState().status, "idle");
});

test("destroy clears rendered fallback controls and their local values", () => {
  const { container, controller } = mounted();
  byAttribute(container, "data-meal-snapshot-action", "manual_entry")[0].click();
  const title = byAttribute(container, "data-fallback-field", "title")[0];
  title.value = "Temporary private text";
  title.dispatch("input");
  controller.destroy();
  assert.equal(byAttribute(container, "data-fallback-field", "title").length, 0);
  assert.equal(container.textContent.includes("Temporary private text"), false);
});

test("fallback forms use safe DOM assignment and expose no save control", () => {
  const { container } = mounted();
  byAttribute(container, "data-meal-snapshot-action", "manual_entry")[0].click();
  assert.match(container.textContent, /Saving is not available/);
  assert.match(container.textContent, /starting-point estimate/i);
  const buttons = descendants(container).filter((node) => node.tagName === "BUTTON");
  assert.deepEqual(buttons.map((button) => button.textContent), ["Clear draft", "Back"]);
  assert.equal(buttons.some((button) => /save|confirm/i.test(button.textContent)), false);
});

test("fallback renderer import only publishes its namespace", async () => {
  const source = await readFile(path.join(__dirname, "../js/meal-snapshot-fallback-renderer.js"), "utf8");
  const traps = [];
  const context = {
    globalThis: null,
    document: new Proxy({}, { get() { traps.push("document"); throw new Error("DOM access"); } }),
    fetch() { traps.push("fetch"); throw new Error("Network access"); },
    localStorage: new Proxy({}, { get() { traps.push("storage"); throw new Error("Storage access"); } })
  };
  context.globalThis = context;
  vm.createContext(context);
  vm.runInContext(source, context);
  assert.deepEqual(traps, []);
  assert.equal(typeof context.MealSnapshotFallbackRenderer.renderManualEntry, "function");
});

test("Phase 1G source contains no network, storage, image reads, filenames, or save path", async () => {
  const files = [
    "../js/meal-snapshot-fallback-renderer.js",
    "../js/meal-snapshot-ui-controller.js"
  ];
  const source = (await Promise.all(files.map((file) => readFile(path.join(__dirname, file), "utf8")))).join("\n");
  const renderSource = await readFile(path.join(__dirname, "../js/render.js"), "utf8");
  const html = await readFile(path.join(__dirname, "../leaner-dashboard.html"), "utf8");
  const config = await readFile(path.join(__dirname, "../js/config.js"), "utf8");

  assert.doesNotMatch(source, /innerHTML|outerHTML|insertAdjacentHTML/i);
  assert.doesNotMatch(source, /FileReader|readAsDataURL|arrayBuffer\s*\(|\.text\s*\(|\.stream\s*\(|createObjectURL|canvas|base64/i);
  assert.doesNotMatch(source, /selectedFile\.name|file\.name|originalFilename|fileName|imageMetadata/i);
  assert.doesNotMatch(source, /fetch\s*\(|XMLHttpRequest|createMealSnapshotClient\s*\(|localStorage|sessionStorage|indexedDB|caches\.|document\.cookie/i);
  assert.doesNotMatch(source, /firebase|firestore|gemini|openai|usda|authorization|bearer/i);
  assert.doesNotMatch(source, /saveFallback|persistFallback|sendFallback|uploadFallback/i);
  assert.doesNotMatch(renderSource, /createMealSnapshotClient\s*\(/);
  assert.match(html, /js\/meal-snapshot-fallback-renderer\.js/);
  assert.match(config, /mealSnapshotBeta\s*:\s*\{[\s\S]*?enabled\s*:\s*false/);
  assert.match(config, /allowConfirmedSave\s*:\s*false/);
});

test("disabled render branch remains exact legacy composition", async () => {
  const renderSource = await readFile(path.join(__dirname, "../js/render.js"), "utf8");
  const context = {
    APP_CONFIG: { features: { mealSnapshotBeta: { enabled: false } } },
    state: { mealSnapshotStep: "upload", mealSnapshotPhoto: "" }
  };
  vm.createContext(context);
  vm.runInContext(renderSource, context);
  const actual = context.mealSnapshot();
  assert.equal(actual, `<section class="screen active meal-snapshot-screen">${context.mealSnapshotUpload()}</section>`);
  assert.equal(actual.includes("mealSnapshotBetaRoot"), false);
  assert.equal(actual.includes("mealSnapshotFile"), true);
});
