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

const {
  DEFAULT_MAX_IMAGE_BYTES,
  SUPPORTED_IMAGE_TYPES,
  createMealSnapshotClient
} = require("../js/meal-snapshot-service.js");

const ENABLED_FEATURE = Object.freeze({
  enabled: true,
  environment: "test",
  endpoint: "https://local.invalid/meal-snapshot",
  allowConfirmedSave: false,
  allowedRoles: ["patient"]
});

const VALID_REVIEW_RESPONSE = Object.freeze({
  schemaVersion: "1.0",
  status: "review_needed",
  providerStatus: "mock_success",
  requiresUserReview: true,
  reviewReasons: ["beta_estimate", "user_confirmation_required"],
  estimate: {
    title: "Test meal",
    confidenceLabel: "Estimated - confirm portions",
    calories: 500,
    protein: 30,
    carbs: 45,
    fat: 20,
    ingredients: [{ name: "Test ingredient", amount: "one portion", confidence: 0.8 }],
    leanerNote: "Confirm anything that looks off.",
    disclaimer: "Nutrition estimates are approximate.",
    source: "mock_estimate"
  },
  manualFallback: {
    available: true,
    actions: ["retry", "manual_entry", "save_note_only"],
    message: "Fallback available."
  },
  shouldSave: false
});

function makeFile(type = "image/jpeg", size = 16, name = "meal.jpg") {
  return new File([new Uint8Array(size)], name, { type });
}

function jsonResponse(payload, status = 200, headers = {}) {
  return new Response(JSON.stringify(payload), {
    status,
    headers: { "content-type": "application/json", ...headers }
  });
}

function makeClient(overrides = {}) {
  return createMealSnapshotClient({
    featureConfig: ENABLED_FEATURE,
    getAuthToken: async () => "test-token-never-log",
    fetchImpl: async () => jsonResponse(VALID_REVIEW_RESPONSE),
    timeoutMs: 100,
    ...overrides
  });
}

test("feature disabled prevents any network or auth call", async () => {
  let fetchCalls = 0;
  let authCalls = 0;
  const client = createMealSnapshotClient({
    featureConfig: { ...ENABLED_FEATURE, enabled: false },
    fetchImpl: async () => { fetchCalls += 1; },
    getAuthToken: async () => { authCalls += 1; return "token"; }
  });
  const result = await client.analyzeMealPhoto({ file: makeFile() });
  assert.equal(result.status, "feature_disabled");
  assert.equal(fetchCalls, 0);
  assert.equal(authCalls, 0);
});

test("missing, unsupported, empty, and oversized files fail before network", async () => {
  let fetchCalls = 0;
  const client = makeClient({ fetchImpl: async () => { fetchCalls += 1; } });
  assert.equal((await client.analyzeMealPhoto()).status, "file_required");
  assert.equal((await client.analyzeMealPhoto({ file: makeFile("image/gif") })).status, "unsupported_media_type");
  assert.equal((await client.analyzeMealPhoto({ file: makeFile("image/png", 0, "empty.png") })).status, "invalid_file");
  assert.equal((await client.analyzeMealPhoto({
    file: { name: "large.jpg", type: "image/jpeg", size: DEFAULT_MAX_IMAGE_BYTES + 1 }
  })).status, "payload_too_large");
  assert.equal(fetchCalls, 0);
});

test("accepts JPEG, PNG, and WebP files", async () => {
  for (const type of SUPPORTED_IMAGE_TYPES) {
    const result = await makeClient().analyzeMealPhoto({ file: makeFile(type) });
    assert.equal(result.status, "review_needed", type);
  }
});

test("requires an auth token provider and a non-empty token", async () => {
  let fetchCalls = 0;
  const noProvider = createMealSnapshotClient({
    featureConfig: ENABLED_FEATURE,
    fetchImpl: async () => { fetchCalls += 1; }
  });
  const emptyToken = makeClient({
    getAuthToken: async () => "",
    fetchImpl: async () => { fetchCalls += 1; }
  });
  assert.equal((await noProvider.analyzeMealPhoto({ file: makeFile() })).status, "auth_required");
  assert.equal((await emptyToken.analyzeMealPhoto({ file: makeFile() })).status, "auth_required");
  assert.equal(fetchCalls, 0);
});

test("sends bearer authentication and multipart FormData without logging either", async () => {
  const events = [];
  let request;
  const client = makeClient({
    logger: (event) => events.push(event),
    fetchImpl: async (url, options) => {
      request = { url, options };
      return jsonResponse(VALID_REVIEW_RESPONSE);
    }
  });
  const result = await client.analyzeMealPhoto({ file: makeFile() });
  assert.equal(result.status, "review_needed");
  assert.equal(request.options.headers.Authorization, "Bearer test-token-never-log");
  assert.ok(request.options.body instanceof FormData);
  assert.ok(request.options.body.get("photo") instanceof File);
  assert.equal(request.options.body.get("photo").name, "meal-photo.jpg");
  assert.equal(Object.keys(request.options.headers).some((key) => key.toLowerCase() === "content-type"), false);
  const serializedEvents = JSON.stringify(events);
  assert.equal(serializedEvents.includes("test-token-never-log"), false);
  assert.equal(serializedEvents.includes("Test meal"), false);
  assert.equal(serializedEvents.includes("Test ingredient"), false);
});

test("normalizes a valid review-needed response without enabling save", async () => {
  const result = await makeClient().analyzeMealPhoto({ file: makeFile() });
  assert.equal(result.ok, true);
  assert.equal(result.status, "review_needed");
  assert.equal(result.requiresUserReview, true);
  assert.equal(result.shouldSave, false);
  assert.equal(result.estimate.title, "Test meal");
});

test("normalizes provider unavailable with manual fallback metadata", async () => {
  const payload = {
    schemaVersion: "1.0",
    status: "fail_safe",
    providerStatus: "provider_unavailable",
    requiresUserReview: true,
    estimate: null,
    manualFallback: VALID_REVIEW_RESPONSE.manualFallback,
    message: "Temporarily unavailable.",
    shouldSave: false
  };
  const result = await makeClient({ fetchImpl: async () => jsonResponse(payload, 503) })
    .analyzeMealPhoto({ file: makeFile() });
  assert.equal(result.status, "provider_unavailable");
  assert.equal(result.providerStatus, "provider_unavailable");
  assert.deepEqual(result.manualFallback.actions, ["retry", "manual_entry", "save_note_only"]);
});

test("normalizes rate limiting and Retry-After", async () => {
  const result = await makeClient({
    fetchImpl: async () => jsonResponse({ error: { code: "RATE_LIMITED" } }, 429, { "retry-after": "45" })
  }).analyzeMealPhoto({ file: makeFile() });
  assert.equal(result.status, "rate_limited");
  assert.equal(result.retryAfterSeconds, 45);
});

test("normalizes unauthorized, oversized, and unsupported server responses", async () => {
  const cases = [
    [401, "unauthorized"],
    [413, "payload_too_large"],
    [415, "unsupported_media_type"]
  ];

  for (const [httpStatus, expectedStatus] of cases) {
    const result = await makeClient({
      fetchImpl: async () => jsonResponse({ error: { code: expectedStatus } }, httpStatus)
    }).analyzeMealPhoto({ file: makeFile() });
    assert.equal(result.httpStatus, httpStatus);
    assert.equal(result.status, expectedStatus);
    assert.equal(result.manualFallback.available, true);
  }
});

test("invalid success and provider responses fail safely", async () => {
  const invalidSuccess = await makeClient({ fetchImpl: async () => jsonResponse({ status: "review_needed" }) })
    .analyzeMealPhoto({ file: makeFile() });
  const invalidUnavailable = await makeClient({ fetchImpl: async () => jsonResponse({ status: "fail_safe" }, 503) })
    .analyzeMealPhoto({ file: makeFile() });
  assert.equal(invalidSuccess.status, "invalid_response");
  assert.equal(invalidUnavailable.status, "request_failed");
  assert.equal(invalidSuccess.manualFallback.available, true);
});

test("unknown response fields are rejected instead of copied into normalized output", async () => {
  const responseWithRawData = {
    ...VALID_REVIEW_RESPONSE,
    estimate: {
      ...VALID_REVIEW_RESPONSE.estimate,
      rawProviderResponse: "must-not-pass-through"
    }
  };
  const result = await makeClient({
    fetchImpl: async () => jsonResponse(responseWithRawData)
  }).analyzeMealPhoto({ file: makeFile() });
  assert.equal(result.status, "invalid_response");
  assert.equal(JSON.stringify(result).includes("must-not-pass-through"), false);
});

test("external abort, timeout, and network errors normalize safely", async () => {
  const abortController = new AbortController();
  abortController.abort();
  const abortResult = await makeClient({
    fetchImpl: async (url, options) => {
      if (options.signal.aborted) throw new DOMException("Aborted", "AbortError");
    }
  }).analyzeMealPhoto({ file: makeFile(), signal: abortController.signal });
  assert.equal(abortResult.status, "aborted");

  const timeoutResult = await makeClient({
    timeoutMs: 1,
    fetchImpl: async (url, options) => new Promise((resolve, reject) => {
      options.signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
    })
  }).analyzeMealPhoto({ file: makeFile() });
  assert.equal(timeoutResult.status, "timeout");

  const networkResult = await makeClient({
    fetchImpl: async () => { throw new TypeError("Offline"); }
  }).analyzeMealPhoto({ file: makeFile() });
  assert.equal(networkResult.status, "network_failure");
});

test("completed requests clear timeout and detach external abort listeners", async () => {
  let scheduled = 0;
  let cleared = 0;
  let added = 0;
  let removed = 0;
  const signal = {
    aborted: false,
    addEventListener(type, listener) {
      assert.equal(type, "abort");
      assert.equal(typeof listener, "function");
      added += 1;
    },
    removeEventListener(type, listener) {
      assert.equal(type, "abort");
      assert.equal(typeof listener, "function");
      removed += 1;
    }
  };
  const result = await makeClient({
    setTimeoutImpl() {
      scheduled += 1;
      return "timeout-id";
    },
    clearTimeoutImpl(timeoutId) {
      assert.equal(timeoutId, "timeout-id");
      cleared += 1;
    }
  }).analyzeMealPhoto({ file: makeFile(), signal });

  assert.equal(result.status, "review_needed");
  assert.equal(scheduled, 1);
  assert.equal(cleared, 1);
  assert.equal(added, 1);
  assert.equal(removed, 1);
});

test("Phase 1A source has no base64, persistence, save, UI, Firebase, or provider integration", async () => {
  const source = await readFile(path.join(__dirname, "../js/meal-snapshot-service.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, /FileReader|readAsDataURL|btoa\s*\(|base64|localStorage|sessionStorage|indexedDB/i);
  assert.doesNotMatch(source, /firebase|firestore|gemini|openai|usda/i);
  assert.doesNotMatch(source, /saveSnapshot|persistSnapshot|writeSnapshot/i);
  assert.equal(html.includes("js/meal-snapshot-service.js"), true);
  assert.match(config, /mealSnapshotBeta\s*:\s*\{[\s\S]*?enabled\s*:\s*false/);
  assert.match(config, /allowConfirmedSave\s*:\s*false/);
});
