import { evaluateCheck, recordFailureDetail } from "./checks.js";
import { safeJson, summarizeHttpTrace, toBodyPreview } from "./http.js";

export function expectStatus(response, expected, label = null) {
  return expectStatusOneOf(response, [expected], label);
}

export function expectStatusOneOf(response, expectedValues, label = null) {
  const values = normalizeExpectedValues(expectedValues);
  const title = normalizeLabel(label, buildExpectedStatusTitle(values));
  const trace = summarizeHttpTrace(response);
  const actual = Number(response?.status ?? 0);

  return evaluateCheck(
    response,
    title,
    (res) => values.includes(Number(res?.status ?? 0)),
    () => buildHttpAssertionDetail({
      kind: "http-assertion",
      title,
      trace,
      expected: values.length === 1 ? values[0] : values,
      actual,
      response,
      message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} expected ${formatExpectedValues(values)}, got ${actual}`,
    })
  );
}

export function expectNotStatus(response, unexpected, label = null) {
  const title = normalizeLabel(label, `status is not ${unexpected}`);
  const trace = summarizeHttpTrace(response);
  const actual = Number(response?.status ?? 0);

  return evaluateCheck(
    response,
    title,
    (res) => Number(res?.status ?? 0) !== unexpected,
    () => buildHttpAssertionDetail({
      kind: "http-assertion",
      title,
      trace,
      expected: { not: unexpected },
      actual,
      response,
      message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} expected not ${unexpected}, got ${actual}`,
    })
  );
}

export function expectJson(response, predicate, label = null) {
  const title = normalizeLabel(label, "response JSON matches expectation");
  const trace = summarizeHttpTrace(response);
  const jsonResult = safeJson(response);

  if (!jsonResult.ok) {
    recordFailureDetail(
      buildHttpAssertionDetail({
        kind: "http-json-assertion",
        title,
        trace,
        expected: "valid JSON response",
        actual: "invalid JSON response",
        response,
        message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} did not return valid JSON`,
      })
    );
    return false;
  }

  return evaluateCheck(
    jsonResult.value,
    title,
    (value) => Boolean(predicate(value)),
    () => ({
      kind: "json-assertion",
      key: buildTraceFailureKey(trace, title),
      title,
      message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} JSON assertion failed`,
      request: trace ? buildRequestSummary(trace) : null,
      response: trace ? buildResponseSummary(trace) : null,
      actual: toValuePreview(jsonResult.value),
    })
  );
}

export function expectJsonPath(response, path, predicate, label = null) {
  const title = normalizeLabel(label, `response JSON path ${path} matches expectation`);
  const trace = summarizeHttpTrace(response);
  const jsonResult = safeJson(response);

  if (!jsonResult.ok) {
    recordFailureDetail(
      buildHttpAssertionDetail({
        kind: "http-json-assertion",
        title,
        trace,
        expected: `valid JSON containing ${path}`,
        actual: "invalid JSON response",
        response,
        message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} did not return valid JSON`,
      })
    );
    return false;
  }

  const actualValue = getJsonPathValue(jsonResult.value, path);
  return evaluateCheck(
    actualValue,
    title,
    (value) => Boolean(predicate(value)),
    () => ({
      kind: "json-path-assertion",
      key: buildTraceFailureKey(trace, title),
      title,
      message: `${trace?.method || "HTTP"} ${trace?.path || "(unknown path)"} JSON path ${path} assertion failed`,
      expected: path,
      actual: toValuePreview(actualValue),
      request: trace ? buildRequestSummary(trace) : null,
      response: trace ? buildResponseSummary(trace) : null,
    })
  );
}

function buildHttpAssertionDetail({ kind, title, trace, expected, actual, response, message }) {
  return {
    kind,
    key: buildTraceFailureKey(trace, title),
    title,
    message,
    expected,
    actual,
    traceId: trace?.id || null,
    request: trace ? buildRequestSummary(trace) : null,
    response: trace ? buildResponseSummary(trace, response) : null,
  };
}

function buildRequestSummary(trace) {
  return {
    id: trace.id,
    requestId: trace.requestId,
    method: trace.method,
    path: trace.path,
    url: trace.url,
    headers: trace.requestHeaders || {},
  };
}

function buildResponseSummary(trace, response = null) {
  return {
    status: (trace.response?.status ?? Number(response?.status ?? 0)) || null,
    contentType: trace.response?.contentType ?? null,
    bodyPreview: trace.response?.bodyPreview ?? toBodyPreview(response),
    bodyTruncated: Boolean(trace.response?.bodyTruncated),
  };
}

function buildTraceFailureKey(trace, title) {
  if (trace?.path && trace?.method) {
    return `${trace.method} ${trace.path} > ${title}`;
  }
  return title;
}

function buildExpectedStatusTitle(expectedValues) {
  return `status is ${formatExpectedValues(expectedValues)}`;
}

function formatExpectedValues(values) {
  if (!Array.isArray(values) || values.length === 0) return "expected";
  if (values.length === 1) return String(values[0]);
  return values.map((value) => String(value)).join(" or ");
}

function normalizeExpectedValues(values) {
  const normalized = (Array.isArray(values) ? values : [values])
    .map((value) => Number(value))
    .filter((value) => Number.isFinite(value));

  if (normalized.length === 0) {
    throw new Error("expectStatusOneOf requires at least one numeric expected status");
  }
  return normalized;
}

function getJsonPathValue(value, path) {
  const tokens = String(path || "")
    .split(".")
    .map((token) => token.trim())
    .filter(Boolean);

  let current = value;
  for (const token of tokens) {
    if (current == null || typeof current !== "object") return undefined;
    current = current[token];
  }
  return current;
}

function toValuePreview(value) {
  if (value == null) return value;
  if (typeof value === "string") return value.length > 240 ? `${value.slice(0, 237)}...` : value;
  try {
    return JSON.stringify(value).slice(0, 240);
  } catch {
    return String(value);
  }
}

function normalizeLabel(value, fallback) {
  const normalized = typeof value === "string" ? value.trim() : "";
  return normalized.length > 0 ? normalized : fallback;
}
