#!/usr/bin/env node
/**
 * Test suite for HeroUI Skill scripts (JavaScript/Node.js).
 *
 * Usage:
 *     # Test against production (default)
 *     node test_skills_scripts.mjs
 *
 *     # Test against staging
 *     node test_skills_scripts.mjs --env staging
 *
 *     # Test against local dev
 *     node test_skills_scripts.mjs --env dev
 *
 *     # Test specific script
 *     node test_skills_scripts.mjs --script list_components
 *
 *     # Verbose output
 *     node test_skills_scripts.mjs -v
 *
 * Environments:
 *     - dev: http://localhost:8787
 *     - staging: https://staging-mcp-api.heroui.com
 *     - production: https://mcp-api.heroui.com
 *
 * Note: Tests are updated for v1 API endpoints which return {results: [...]} format
 * for batch requests (multiple components).
 */

import {execFile} from "child_process";
import {dirname, join} from "path";
import {fileURLToPath} from "url";
import {promisify} from "util";

const execFileAsync = promisify(execFile);

// Environment configurations
const ENVIRONMENTS = {
  dev: "http://localhost:8787",
  production: "https://mcp-api.heroui.com",
  staging: "https://staging-mcp-api.heroui.com",
};

// Get script directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const SCRIPTS_DIR = join(__dirname, "..", "..", "skills/heroui-react/scripts");
const NODE = "node";

/**
 * Result of a single test.
 */
class TestResult {
  constructor(name, passed, duration, output, error = null) {
    this.name = name;
    this.passed = passed;
    this.duration = duration;
    this.output = output;
    this.error = error;
  }
}

/**
 * Run a script and return (stdout, stderr, return_code, duration).
 */
async function runScript(scriptName, args = null, envVars = null, timeout = 30000) {
  const scriptPath = join(SCRIPTS_DIR, scriptName);
  const cmd = [NODE, scriptPath, ...(args || [])];

  const env = {...process.env};

  if (envVars) {
    Object.assign(env, envVars);
  }

  const start = Date.now();

  try {
    // Use AbortController for timeout (Node 15+)
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const {stderr, stdout} = await execFileAsync(cmd[0], cmd.slice(1), {
        encoding: "utf8",
        env,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);
      const duration = (Date.now() - start) / 1000;

      return [stdout, stderr, 0, duration];
    } catch (error) {
      clearTimeout(timeoutId);

      if (error.signal === "SIGABRT" || error.code === "ABORT_ERR") {
        const duration = (Date.now() - start) / 1000;

        return ["", `Timeout after ${timeout / 1000}s`, -1, duration];
      }

      // execFile throws error with stdout/stderr in error object
      const duration = (Date.now() - start) / 1000;

      return [error.stdout || "", error.stderr || "", error.code || -1, duration];
    }
  } catch (error) {
    const duration = (Date.now() - start) / 1000;

    return ["", String(error), -1, duration];
  }
}

async function testListComponents(apiBase, verbose = false) {
  const name = "list_components";
  const envVars = {HEROUI_API_BASE: apiBase};

  const [stdout, _stderr, _code, duration] = await runScript("list_components.mjs", null, envVars);

  // Validate output
  try {
    const data = JSON.parse(stdout);
    const hasComponents = "components" in data && data.components.length > 0;
    const hasVersion = "latestVersion" in data;

    if (!hasComponents) {
      return new TestResult(name, false, duration, stdout, "No components in response");
    }

    if (!hasVersion) {
      return new TestResult(name, false, duration, stdout, "No latestVersion in response");
    }

    if (verbose) {
      console.log(`  Found ${data.components.length} components, version: ${data.latestVersion}`);
    }

    return new TestResult(name, true, duration, `Found ${data.components.length} components`);
  } catch (e) {
    return new TestResult(name, false, duration, stdout, `Invalid JSON: ${e.message}`);
  }
}

async function testGetComponentDocs(apiBase, verbose = false) {
  const name = "get_component_docs";
  const envVars = {HEROUI_API_BASE: apiBase};

  // Test with single component
  const [stdout, stderr, _code, duration] = await runScript(
    "get_component_docs.mjs",
    ["Button"],
    envVars,
  );

  // Should contain MDX content
  const hasContent = stdout.length > 100;

  if (!hasContent) {
    return new TestResult(name, false, duration, stdout.slice(0, 200), "No content returned");
  }

  if (verbose) {
    console.log(`  Received ${stdout.length} chars of documentation`);

    if (stderr.toLowerCase().includes("fallback")) {
      console.log(`  Note: Used fallback method`);
    }
  }

  return new TestResult(name, true, duration, `Received ${stdout.length} chars`);
}

async function testGetComponentDocsMultiple(apiBase, verbose = false) {
  const name = "get_component_docs_multiple";
  const envVars = {HEROUI_API_BASE: apiBase};

  const [stdout, _stderr, _code, duration] = await runScript(
    "get_component_docs.mjs",
    ["Button", "Card"],
    envVars,
  );

  // v1 API returns {results: [...]} format
  try {
    const data = JSON.parse(stdout);

    // Check for v1 API format: {results: [...]}
    if (data.results && Array.isArray(data.results) && data.results.length === 2) {
      if (verbose) {
        console.log(`  Received docs for ${data.results.length} components (v1 format)`);
      }

      return new TestResult(name, true, duration, `Received ${data.results.length} component docs`);
    }

    // Fallback: check if it's a plain array (fallback format)
    if (Array.isArray(data) && data.length === 2) {
      if (verbose) {
        console.log(`  Received docs for ${data.length} components (fallback format)`);
      }

      return new TestResult(name, true, duration, `Received ${data.length} component docs`);
    }

    return new TestResult(
      name,
      false,
      duration,
      stdout.slice(0, 200),
      "Expected {results: [...]} or array of 2 results",
    );
  } catch (e) {
    // If not JSON, might be concatenated MDX (also acceptable)
    if (stdout.length > 200) {
      return new TestResult(name, true, duration, `Received ${stdout.length} chars (MDX format)`);
    }

    return new TestResult(name, false, duration, stdout.slice(0, 200), "Invalid response format");
  }
}

async function testGetSource(apiBase, verbose = false) {
  const name = "get_source";
  const envVars = {HEROUI_API_BASE: apiBase};

  const [stdout, _stderr, _code, duration] = await runScript("get_source.mjs", ["Button"], envVars);

  // Should contain TypeScript/React code
  const hasContent = stdout.length > 100;

  if (!hasContent) {
    return new TestResult(name, false, duration, stdout.slice(0, 200), "No content returned");
  }

  if (verbose) {
    console.log(`  Received ${stdout.length} chars of source code`);

    if (stdout.includes("GitHub:")) {
      console.log(`  Includes GitHub URL`);
    }
  }

  return new TestResult(name, true, duration, `Received ${stdout.length} chars`);
}

async function testGetStyles(apiBase, verbose = false) {
  const name = "get_styles";
  const envVars = {HEROUI_API_BASE: apiBase};

  const [stdout, _stderr, _code, duration] = await runScript("get_styles.mjs", ["Button"], envVars);

  // Should contain CSS
  const hasBem = stdout.includes("--"); // BEM modifiers like .button--primary
  const hasContent = stdout.length > 100;

  if (!hasContent) {
    return new TestResult(name, false, duration, stdout.slice(0, 200), "No content returned");
  }

  if (verbose) {
    console.log(`  Received ${stdout.length} chars of CSS`);

    if (hasBem) {
      console.log(`  Contains BEM classes`);
    }
  }

  return new TestResult(name, true, duration, `Received ${stdout.length} chars`);
}

async function testGetTheme(apiBase, verbose = false) {
  const name = "get_theme";
  const envVars = {HEROUI_API_BASE: apiBase};

  const [stdout, _stderr, _code, duration] = await runScript("get_theme.mjs", null, envVars);

  // Should contain CSS variables
  const hasOklch = stdout.toLowerCase().includes("oklch");
  const hasContent = stdout.length > 100;

  if (!hasContent) {
    return new TestResult(name, false, duration, stdout.slice(0, 200), "No content returned");
  }

  if (verbose) {
    console.log(`  Received ${stdout.length} chars of theme variables`);

    if (hasOklch) {
      console.log(`  Uses oklch color format`);
    }
  }

  return new TestResult(name, true, duration, `Received ${stdout.length} chars`);
}

async function testGetDocs(apiBase, verbose = false) {
  const name = "get_docs";
  const envVars = {HEROUI_API_BASE: apiBase};

  const [stdout, _stderr, _code, duration] = await runScript(
    "get_docs.mjs",
    ["/docs/react/getting-started/theming"],
    envVars,
  );

  // Should contain MDX content about theming
  const hasContent = stdout.length > 100;

  if (!hasContent) {
    return new TestResult(name, false, duration, stdout.slice(0, 200), "No content returned");
  }

  if (verbose) {
    console.log(`  Received ${stdout.length} chars of documentation`);
  }

  return new TestResult(name, true, duration, `Received ${stdout.length} chars`);
}

async function testGetDocsUsage(apiBase, verbose = false) {
  const name = "get_docs_usage";
  const envVars = {HEROUI_API_BASE: apiBase};

  const [_stdout, stderr, _code, duration] = await runScript("get_docs.mjs", null, envVars);

  // Should show usage in stderr
  const hasUsage =
    stderr.toLowerCase().includes("usage") || stderr.toLowerCase().includes("example");

  if (!hasUsage) {
    return new TestResult(name, false, duration, stderr.slice(0, 200), "No usage info shown");
  }

  if (verbose) {
    console.log(`  Shows usage instructions`);
  }

  return new TestResult(name, true, duration, "Shows usage instructions");
}

async function testErrorHandling(apiBase, verbose = false) {
  const name = "error_handling";
  const envVars = {HEROUI_API_BASE: apiBase};

  const [stdout, stderr, _code, duration] = await runScript(
    "get_component_docs.mjs",
    ["NonExistentComponent12345"],
    envVars,
  );

  // Should handle gracefully (either error message or empty result)
  const combined = (stdout + stderr).toLowerCase();

  if (verbose) {
    if (combined.includes("error")) {
      console.log(`  Returns error message for invalid component`);
    } else if (stderr.toLowerCase().includes("fallback")) {
      console.log(`  Uses fallback for invalid component`);
    }
  }

  return new TestResult(name, true, duration, "Handles invalid input gracefully");
}

// All tests
const ALL_TESTS = {
  error_handling: testErrorHandling,
  get_component_docs: testGetComponentDocs,
  get_component_docs_multiple: testGetComponentDocsMultiple,
  get_docs: testGetDocs,
  get_docs_usage: testGetDocsUsage,
  get_source: testGetSource,
  get_styles: testGetStyles,
  get_theme: testGetTheme,
  list_components: testListComponents,
};

async function runTests(env, scripts = null, verbose = false) {
  const apiBase = ENVIRONMENTS[env] || ENVIRONMENTS.production;
  const results = [];

  const testsToRun = Object.fromEntries(
    Object.entries(ALL_TESTS).filter(([k]) => !scripts || scripts.includes(k)),
  );

  console.log(`\n${"=".repeat(60)}`);
  console.log(`HeroUI Skill Scripts Test Suite (JavaScript)`);
  console.log(`${"=".repeat(60)}`);
  console.log(`Environment: ${env} (${apiBase})`);
  console.log(`Tests to run: ${Object.keys(testsToRun).length}`);
  console.log(`${"=".repeat(60)}\n`);

  for (const [name, testFunc] of Object.entries(testsToRun)) {
    process.stdout.write(`Running: ${name}... `);
    const result = await testFunc(apiBase, verbose);

    results.push(result);

    const status = result.passed ? "✓ PASS" : "✗ FAIL";

    console.log(`${status} (${result.duration.toFixed(2)}s)`);

    if (result.error && (verbose || !result.passed)) {
      console.log(`  Error: ${result.error}`);
    }
  }

  return results;
}

function printSummary(results) {
  const passed = results.filter((r) => r.passed).length;
  const failed = results.filter((r) => !r.passed).length;
  const totalTime = results.reduce((sum, r) => sum + r.duration, 0);

  console.log(`\n${"=".repeat(60)}`);
  console.log(`Test Summary`);
  console.log(`${"=".repeat(60)}`);
  console.log(`Total:  ${results.length}`);
  console.log(`Passed: ${passed}`);
  console.log(`Failed: ${failed}`);
  console.log(`Time:   ${totalTime.toFixed(2)}s`);
  console.log(`${"=".repeat(60)}`);

  if (failed > 0) {
    console.log(`\nFailed tests:`);

    for (const r of results) {
      if (!r.passed) {
        console.log(`  - ${r.name}: ${r.error}`);
      }
    }
  }

  return failed === 0;
}

function parseArgs() {
  const args = process.argv.slice(2);
  const options = {
    env: "production",
    list: false,
    script: [],
    verbose: false,
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];

    if (arg === "--env" || arg === "-e") {
      options.env = args[++i];
    } else if (arg === "--script" || arg === "-s") {
      options.script.push(args[++i]);
    } else if (arg === "--verbose" || arg === "-v") {
      options.verbose = true;
    } else if (arg === "--list" || arg === "-l") {
      options.list = true;
    }
  }

  return options;
}

async function main() {
  const options = parseArgs();

  if (options.list) {
    console.log("Available tests:");

    for (const name of Object.keys(ALL_TESTS)) {
      console.log(`  - ${name}`);
    }

    return 0;
  }

  const results = await runTests(
    options.env,
    options.script.length > 0 ? options.script : null,
    options.verbose,
  );
  const success = printSummary(results);

  return success ? 0 : 1;
}

// Run main and exit with code
main()
  .then((code) => {
    process.exit(code);
  })
  .catch((error) => {
    console.error("Error:", error);
    process.exit(1);
  });
