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

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, preventDefault() {} }); }
  click() { this.dispatch("click"); }
}

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

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

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 safeFile() {
  const file = { type: "image/jpeg", size: 128 };
  Object.defineProperty(file, "name", { get() { throw new Error("Filename read"); } });
  Object.defineProperty(file, "arrayBuffer", { get() { throw new Error("Byte read"); } });
  return file;
}

function createMountedController() {
  const documentRef = new FakeDocument();
  const container = documentRef.createElement("div");
  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
  });
  return { container, controller, events };
}

test("mock estimate factory returns fresh local-only drafts", () => {
  const first = MealSnapshotMockEstimate.createMockMealSnapshotEstimate();
  const second = MealSnapshotMockEstimate.createMockMealSnapshotEstimate();
  assert.notEqual(first, second);
  assert.notEqual(first.ingredients, second.ingredients);
  assert.equal(first.source, "local_mock");
  assert.equal(first.requiresReview, true);
  first.title = "Changed locally";
  assert.notEqual(second.title, "Changed locally");
});

test("review renderer shows approved mock fields, disclaimer, and no save control", () => {
  const documentRef = new FakeDocument();
  const container = documentRef.createElement("div");
  const draft = MealSnapshotMockEstimate.createMockMealSnapshotEstimate();
  MealSnapshotReviewRenderer.renderMealSnapshotMockReview(container, draft, { documentRef });

  assert.match(container.textContent, /Estimated Meal Snapshot/);
  assert.match(container.textContent, /Sample estimate for testing/);
  assert.match(container.textContent, /not based on your photo/);
  assert.match(container.textContent, /Real AI analysis is not connected yet/);
  assert.match(container.textContent, /This is an estimate based on your photo/);
  const titleInput = byAttribute(container, "data-review-field", "title")[0];
  assert.equal(titleInput.value, "Turkey egg croissant");
  assert.equal(titleInput.attributes.autocomplete, "off");
  assert.match(container.textContent, /Protein/);
  const buttons = descendants(container).filter((node) => node.tagName === "BUTTON");
  assert.deepEqual(buttons.map((button) => button.textContent), ["Reset sample", "Back to photo check"]);
  assert.equal(buttons.some((button) => /save|confirm/i.test(button.textContent)), false);
});

test("review renderer uses value/textContent and ignores unknown raw fields", () => {
  const documentRef = new FakeDocument();
  const container = documentRef.createElement("div");
  const draft = {
    ...MealSnapshotMockEstimate.createMockMealSnapshotEstimate(),
    rawProviderResponse: "private provider response",
    originalFilename: "patient-jane-breakfast.jpg",
    imageData: "data:image/private"
  };
  MealSnapshotReviewRenderer.renderMealSnapshotMockReview(container, draft, { documentRef });
  const rendered = container.textContent;
  assert.equal(rendered.includes("private provider response"), false);
  assert.equal(rendered.includes("patient-jane"), false);
  assert.equal(rendered.includes("data:image"), false);
});

test("review edits update only the controller's in-memory mock draft", () => {
  const { container, controller, events } = createMountedController();
  controller.input.files = [safeFile()];
  controller.input.dispatch("change");
  const reviewButton = byAttribute(container, "data-meal-snapshot-action", "review_mock_estimate")[0];
  assert.ok(reviewButton);
  reviewButton.click();
  assert.equal(controller.getState().status, "draft_estimate");

  const titleInput = byAttribute(container, "data-review-field", "title")[0];
  assert.ok(titleInput);
  titleInput.value = "Edited local sample";
  titleInput.dispatch("input");
  assert.equal(controller.getDraft().title, "Edited local sample");
  assert.equal(JSON.stringify(events).includes("Edited local sample"), false);
  assert.equal(events.at(-1).canSaveConfirmed, false);
});

test("ingredient and macro edits remain local and reset restores the sample", () => {
  const { container, controller } = createMountedController();
  controller.input.files = [safeFile()];
  controller.input.dispatch("change");
  byAttribute(container, "data-meal-snapshot-action", "review_mock_estimate")[0].click();

  const protein = byAttribute(container, "data-review-field", "protein")[0];
  protein.value = "42";
  protein.dispatch("input");
  assert.equal(controller.getDraft().protein, 42);

  const ingredientAmount = byAttribute(container, "data-ingredient-field", "amount")[0];
  ingredientAmount.value = "adjusted portion";
  ingredientAmount.dispatch("input");
  assert.equal(controller.getDraft().ingredients[0].amount, "adjusted portion");

  byAttribute(container, "data-review-action", "reset")[0].click();
  assert.notEqual(controller.getDraft().protein, 42);
  assert.notEqual(controller.getDraft().ingredients[0].amount, "adjusted portion");
});

test("back discards the local draft and returns to local-file-ready", () => {
  const { container, controller } = createMountedController();
  controller.input.files = [safeFile()];
  controller.input.dispatch("change");
  byAttribute(container, "data-meal-snapshot-action", "review_mock_estimate")[0].click();
  assert.ok(controller.getDraft());
  byAttribute(container, "data-review-action", "back")[0].click();
  assert.equal(controller.getState().status, "local_file_ready");
  assert.equal(controller.getDraft(), null);
  assert.equal(controller.buildReviewedDraft(), null);
});

test("controller builds a sanitized local draft without emitting its contents", () => {
  const { container, controller, events } = createMountedController();
  controller.input.files = [safeFile()];
  controller.input.dispatch("change");
  byAttribute(container, "data-meal-snapshot-action", "review_mock_estimate")[0].click();
  const titleInput = byAttribute(container, "data-review-field", "title")[0];
  titleInput.value = "Edited reviewed sample";
  titleInput.dispatch("input");

  const beforeBuildEventCount = events.length;
  const reviewed = controller.buildReviewedDraft();
  assert.equal(reviewed.title, "Edited reviewed sample");
  assert.equal(reviewed.review.userEdited, true);
  assert.equal(reviewed.canSave, false);
  assert.equal(reviewed.shouldSave, false);
  assert.equal(reviewed.persisted, false);
  assert.equal(events.length, beforeBuildEventCount);
  assert.equal(JSON.stringify(events).includes("Edited reviewed sample"), false);
});

test("reset creates an unedited reviewed draft and destroy removes it", () => {
  const { container, controller } = createMountedController();
  controller.input.files = [safeFile()];
  controller.input.dispatch("change");
  byAttribute(container, "data-meal-snapshot-action", "review_mock_estimate")[0].click();
  const titleInput = byAttribute(container, "data-review-field", "title")[0];
  titleInput.value = "Temporary edit";
  titleInput.dispatch("input");
  assert.equal(controller.buildReviewedDraft().review.userEdited, true);

  byAttribute(container, "data-review-action", "reset")[0].click();
  assert.equal(controller.buildReviewedDraft().review.userEdited, false);
  controller.destroy();
  assert.equal(controller.buildReviewedDraft(), null);
});

test("Phase 1E modules contain no network, persistence, image, filename, or confirmed-save path", async () => {
  const files = [
    "../js/meal-snapshot-mock-estimate.js",
    "../js/meal-snapshot-review-renderer.js",
    "../js/meal-snapshot-reviewed-draft.js"
  ];
  const source = (await Promise.all(files.map((file) => readFile(path.join(__dirname, file), "utf8")))).join("\n");
  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(source, /innerHTML|insertAdjacentHTML|FileReader|readAsDataURL|arrayBuffer\s*\(|\.text\s*\(|\.stream\s*\(|createObjectURL|canvas|base64/i);
  assert.doesNotMatch(source, /fetch\s*\(|XMLHttpRequest|createMealSnapshotClient|localStorage|sessionStorage|indexedDB/i);
  assert.doesNotMatch(source, /firebase|firestore|gemini|openai|usda|authorization|bearer/i);
  assert.doesNotMatch(controllerSource, /createMealSnapshotClient\s*\(/);
  assert.doesNotMatch(renderSource, /createMealSnapshotClient\s*\(/);
  assert.match(html, /js\/meal-snapshot-mock-estimate\.js/);
  assert.match(html, /js\/meal-snapshot-review-renderer\.js/);
  assert.match(config, /mealSnapshotBeta\s*:\s*\{[\s\S]*?enabled\s*:\s*false/);
  assert.match(config, /allowConfirmedSave\s*:\s*false/);
});
