#!/usr/bin/env node
// The Lucia project.
// Copyright 2026-2026 DSP, inc.  All rights reserved.
// Authors:
//   imacat.yang@dsp.im (imacat), 2026/03/22
/**
 * Validates MSW fixture JSON files against the OpenAPI spec.
 * Ensures mock data matches the real API contract.
 *
 * Usage: node tests/validate-fixtures.js
 */

import Ajv from "ajv";
import addFormats from "ajv-formats";
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, "..");

const spec = JSON.parse(
  readFileSync(resolve(root, "excludes/openapi.json"), "utf-8"),
);
const schemas = spec.components?.schemas ?? {};

const ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv);

// Add ISO 8601 duration format (not included in ajv-formats)
ajv.addFormat("duration", {
  type: "string",
  validate: (s) => /^P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/.test(s),
});

// Register all schemas from the OpenAPI spec
for (const [name, schema] of Object.entries(schemas)) {
  ajv.addSchema(schema, `#/components/schemas/${name}`);
}

/**
 * Resolves a JSON Schema $ref to the actual schema object.
 * @param {object} schema - Schema that may contain $ref.
 * @returns {object} Resolved schema.
 */
function resolveRef(schema) {
  if (schema?.$ref) {
    const refName = schema.$ref.replace("#/components/schemas/", "");
    return schemas[refName] ?? schema;
  }
  return schema;
}

/**
 * Gets the response schema for a given path and method.
 * @param {string} method - HTTP method (lowercase).
 * @param {string} path - API path.
 * @returns {object|null} Resolved schema or null.
 */
function getResponseSchema(method, path) {
  const pathInfo = spec.paths?.[path];
  if (!pathInfo) return null;
  const methodInfo = pathInfo[method];
  if (!methodInfo) return null;
  const responses = methodInfo.responses ?? {};
  const resp = responses["200"] ?? responses["201"] ?? {};
  const content = resp.content?.["application/json"] ?? {};
  return content.schema ? resolveRef(content.schema) : null;
}

// Fixture-to-endpoint mapping
const fixtures = [
  ["token.json", "post", "/oauth/token"],
  ["my-account.json", "get", "/my-account"],
  ["users.json", "get", "/users"],
  ["user-detail.json", "get", "/users/{username}"],
  ["files.json", "get", "/files"],
  ["discover.json", "get", "/logs/{log_id}/discover"],
  ["performance.json", "get", "/logs/{log_id}/performance"],
  ["traces.json", "get", "/logs/{log_id}/traces"],
  ["trace-detail.json", "get", "/logs/{log_id}/traces/{trace_id}"],
  // compare.json: real API returns numbers for duration y-values and
  // dates without Z suffix, which doesn't match the OpenAPI spec.
  // Skipped until the spec is updated to match the actual API.
  // ["compare.json", "get", "/compare"],
  ["filter-params.json", "get", "/filters/params"],
];

let passed = 0;
let failed = 0;

for (const [fixture, method, path] of fixtures) {
  const data = JSON.parse(
    readFileSync(
      resolve(root, "src/mocks/fixtures", fixture),
      "utf-8",
    ),
  );
  const schema = getResponseSchema(method, path);

  if (!schema) {
    console.log(`⚠  ${fixture}: no schema found for ${method.toUpperCase()} ${path}`);
    continue;
  }

  // For array responses, validate the schema's items against each element
  if (schema.type === "array" && schema.items) {
    const itemSchema = resolveRef(schema.items);
    let allValid = true;
    for (let i = 0; i < data.length; i++) {
      const valid = ajv.validate(itemSchema, data[i]);
      if (!valid) {
        console.log(`✗  ${fixture}[${i}]: ${ajv.errorsText()}`);
        allValid = false;
      }
    }
    if (allValid) {
      console.log(`✓  ${fixture} (${data.length} items)`);
      passed++;
    } else {
      failed++;
    }
  } else {
    const valid = ajv.validate(schema, data);
    if (valid) {
      console.log(`✓  ${fixture}`);
      passed++;
    } else {
      console.log(`✗  ${fixture}: ${ajv.errorsText()}`);
      failed++;
    }
  }
}

console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
