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.accept = "";
    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 innerHTML 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);
  }
}

const ENABLED_FEATURE = Object.freeze({
  enabled: true,
  allowConfirmedSave: false
});

const DISABLED_FEATURE = Object.freeze({
  enabled: false,
  allowConfirmedSave: false
});

function fixture() {
  const documentRef = new FakeDocument();
  return { documentRef, container: documentRef.createElement("div") };
}

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

function findOne(container, predicate) {
  const matches = descendants(container).filter(predicate);
  assert.equal(matches.length, 1);
  return matches[0];
}

function safeFile(type = "image/jpeg", size = 128) {
  const file = { type, size };
  Object.defineProperty(file, "name", {
    get() { throw new Error("Filename must not be read"); }
  });
  Object.defineProperty(file, "arrayBuffer", {
    get() { throw new Error("Image bytes must not be read"); }
  });
  return file;
}

function mount(overrides = {}) {
  const { container, documentRef } = fixture();
  const events = [];
  const controller = mountMealSnapshotBetaController({
    container,
    featureConfig: ENABLED_FEATURE,
    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,
    ...overrides
  });
  return { container, documentRef, controller, events };
}

test("disabled controller leaves the legacy container untouched", () => {
  const { container, documentRef } = fixture();
  const legacy = documentRef.createElement("div");
  legacy.textContent = "Existing legacy mock UI";
  container.append(legacy);

  const controller = mountMealSnapshotBetaController({
    container,
    featureConfig: DISABLED_FEATURE,
    uiStateApi: MealSnapshotUiState,
    rendererApi: MealSnapshotUiRenderer,
    mockEstimateApi: MealSnapshotMockEstimate,
    reviewRendererApi: MealSnapshotReviewRenderer,
    reviewedDraftApi: MealSnapshotReviewedDraft,
    fallbackRendererApi: MealSnapshotFallbackRenderer,
    validateFile: MealSnapshotService.validateFile,
    documentRef
  });

  assert.equal(controller.mounted, false);
  assert.equal(container.textContent, "Existing legacy mock UI");
  assert.equal(container.children.length, 1);
});

test("enabled controller mounts a hidden local-validation file input", () => {
  const { container, controller } = mount();
  const input = findOne(container, (node) => node.tagName === "INPUT");
  assert.equal(controller.mounted, true);
  assert.equal(input.type, "file");
  assert.equal(input.accept, "image/jpeg,image/png,image/webp");
  assert.equal(input.className, "meal-snapshot-beta-file-input");
  assert.match(container.textContent, /Choose a meal photo/);
  assert.equal(container.textContent.includes(".jpg"), false);
});

test("valid selection reads only type and size, clears the input, and shows local-ready state", () => {
  const { container, controller, events } = mount();
  const input = controller.input;
  input.files = [safeFile("image/png", 512)];
  input.value = "browser-managed-value";
  input.dispatch("change");

  assert.equal(controller.getState().status, "local_file_ready");
  assert.equal(input.value, "");
  assert.match(container.textContent, /Photo ready for Meal Snapshot Beta/);
  assert.match(container.textContent, /Testing mode/);
  assert.match(container.textContent, /checked locally only/);
  assert.match(container.textContent, /not analyzed or saved/);
  assert.equal(container.textContent.includes("browser-managed-value"), false);
  assert.equal(events.at(-1).status, "local_file_ready");
});

test("missing, invalid, and oversized selections render validation states without network", () => {
  const { controller } = mount();
  const input = controller.input;

  input.files = [];
  input.dispatch("change");
  assert.equal(controller.getState().status, "no_file_selected");

  input.files = [safeFile("image/gif", 128)];
  input.dispatch("change");
  assert.equal(controller.getState().status, "invalid_file");

  input.files = [safeFile("image/jpeg", MealSnapshotService.DEFAULT_MAX_IMAGE_BYTES + 1)];
  input.dispatch("change");
  assert.equal(controller.getState().status, "oversized_file");
});

test("controller events contain only status and capability booleans", () => {
  const { controller, events } = mount();
  controller.input.files = [safeFile()];
  controller.input.dispatch("change");
  const event = events.at(-1);
  assert.deepEqual(Object.keys(event).sort(), [
    "canManualEntry",
    "canNoteOnly",
    "canRetry",
    "canSaveConfirmed",
    "event",
    "status"
  ]);
  assert.equal(event.canSaveConfirmed, false);
});

test("manual and note-only controls open local forms and confirmed save is unreachable", () => {
  const { container, controller, events } = mount();
  let buttons = descendants(container).filter((node) => node.tagName === "BUTTON");
  const manual = buttons.find((button) => button.attributes["data-meal-snapshot-action"] === "manual_entry");
  assert.ok(manual);
  manual.click();
  assert.equal(controller.getState().status, "manual_entry");
  assert.match(container.textContent, /This stays on this screen only/);
  const title = findOne(container, (node) => node.attributes["data-fallback-field"] === "title");
  title.value = "Private typed meal";
  title.dispatch("input");
  assert.equal(JSON.stringify(events).includes("Private typed meal"), false);

  buttons = descendants(container).filter((node) => node.tagName === "BUTTON");
  const back = buttons.find((button) => button.attributes["data-fallback-action"] === "back");
  back.click();
  buttons = descendants(container).filter((node) => node.tagName === "BUTTON");
  buttons.find((button) => button.attributes["data-meal-snapshot-action"] === "save_note_only").click();
  assert.equal(controller.getState().status, "note_only");
  const note = findOne(container, (node) => node.attributes["data-fallback-field"] === "note");
  note.value = "Private note";
  note.dispatch("input");
  assert.equal(JSON.stringify(events).includes("Private note"), false);
  buttons = descendants(container).filter((node) => node.tagName === "BUTTON");
  assert.equal(buttons.some((button) => /confirm|save snapshot/i.test(button.textContent)), false);
  assert.equal(controller.getState().canSaveConfirmed, false);
});

test("destroy detaches the file listener without retaining selected file state", () => {
  const { container, controller } = mount();
  const input = controller.input;
  assert.equal(typeof input.listeners.change, "function");
  controller.destroy();
  assert.equal(input.listeners.change, undefined);
  assert.equal(input.value, "");
  assert.equal(controller.getState().canSaveConfirmed, false);
  const stateHost = findOne(container, (node) => node.className === "meal-snapshot-beta-state-host");
  assert.equal(stateHost.children.length, 0);
});

test("Phase 1D source has no byte reads, previews, network, persistence, or filename access", async () => {
  const controllerSource = await readFile(path.join(__dirname, "../js/meal-snapshot-ui-controller.js"), "utf8");
  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(controllerSource, /FileReader|readAsDataURL|arrayBuffer\s*\(|createObjectURL|data:image|base64/i);
  assert.doesNotMatch(controllerSource, /\.name\b|filename|fileName/i);
  assert.doesNotMatch(controllerSource, /fetch\s*\(|XMLHttpRequest|createMealSnapshotClient/i);
  assert.doesNotMatch(controllerSource, /localStorage|sessionStorage|indexedDB|firebase|firestore|gemini|openai|usda/i);
  assert.doesNotMatch(controllerSource, /saveSnapshot|persistSnapshot|writeSnapshot/i);
  assert.match(renderSource, /MealSnapshotUiController\.mountMealSnapshotBetaController/);
  assert.match(html, /js\/meal-snapshot-service\.js/);
  assert.match(html, /js\/meal-snapshot-ui-controller\.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();
  const expected = `<section class="screen active meal-snapshot-screen">${context.mealSnapshotUpload()}</section>`;
  assert.equal(actual, expected);
  assert.equal(actual.includes("mealSnapshotBetaRoot"), false);
  assert.equal(actual.includes("mealSnapshotFile"), true);
});
