/**
 * Workflow MCP tool test runner.
 *
 * Connects to the workflows MCP server via the SDK Client and exercises
 * every tool through the MCP protocol — same path production uses.
 *
 * Requires: Neo4j running, MCP server built (dist/index.js), PLATFORM_ROOT set.
 *
 *   SETUP
 *     ├─ connect to MCP server
 *     └─ generate unique test account ID
 *
 *   TESTS (sequential — each depends on prior state)
 *     ├─ Creation: valid workflow, missing plugin, cycle detection, step limit
 *     ├─ Type enforcement: reject "skill" and "subagent" types
 *     ├─ Timeout validation: reject 0, reject 121, accept 120, accept omitted
 *     ├─ Get / List / Update
 *     ├─ Validation: re-validate after conditions change
 *     ├─ Execution: status checks (draft/paused rejected)
 *     ├─ Delete: cascade verification
 *     └─ Run history
 *
 *   TEARDOWN
 *     └─ clean up test data from Neo4j
 */

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { randomUUID } from "node:crypto";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const serverPath = resolve(__dirname, "dist", "index.js");

// Unique account ID for test isolation — all test data scoped to this ID
const TEST_ACCOUNT_ID = `test-${randomUUID()}`;

let client;
let transport;
let passed = 0;
let failed = 0;
const failures = [];

// ─────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────

function pass(name) {
  passed++;
  console.log(`  PASS: ${name}`);
}

function fail(name, reason) {
  failed++;
  failures.push({ name, reason });
  console.error(`  FAIL: ${name} — ${reason}`);
}

async function callTool(name, args) {
  const result = await client.callTool({ name, arguments: args });
  const contentArr = Array.isArray(result.content) ? result.content : [];
  const text = contentArr
    .filter((c) => c.type === "text")
    .map((c) => c.text)
    .join("\n");
  return { text, isError: result.isError === true };
}

// ─────────────────────────────────────────────────────────────
// Setup
// ─────────────────────────────────────────────────────────────

async function setup() {
  const env = { ...process.env, ACCOUNT_ID: TEST_ACCOUNT_ID };

  transport = new StdioClientTransport({
    command: process.execPath,
    args: [serverPath],
    env,
    stderr: "pipe",
  });

  client = new Client({ name: "workflow-test-runner", version: "1.0.0" });
  await client.connect(transport);
  console.log(`Connected to MCP server (account: ${TEST_ACCOUNT_ID})\n`);
}

// ─────────────────────────────────────────────────────────────
// Teardown — clean up all test data
// ─────────────────────────────────────────────────────────────

async function teardown() {
  try {
    // Delete all workflows created during testing
    const listResult = await callTool("workflow-list", {});
    if (!listResult.isError && listResult.text.includes("workflow(s)")) {
      // Extract workflow IDs from the list output
      const idMatches = listResult.text.matchAll(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/g);
      for (const match of idMatches) {
        try {
          await callTool("workflow-delete", { workflowId: match[1] });
        } catch {
          // best-effort cleanup
        }
      }
    }
  } catch {
    // best-effort
  }

  try {
    await client.close();
  } catch {
    // best-effort
  }
}

// ─────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────

async function testCreation() {
  console.log("=== Creation Tests ===");

  // Test 1: Valid workflow with existing plugin creates as active
  const r1 = await callTool("workflow-create", {
    name: "Test Valid Workflow",
    description: "A test workflow with a valid plugin reference",
    steps: [
      {
        type: "tool",
        plugin: "memory",
        tool: "memory-search",
        params: { query: "test" },
        label: "Search memory",
        outputKey: "searchResult",
      },
    ],
  });

  if (!r1.isError && r1.text.includes("status: active")) {
    pass("Valid workflow creates with status=active");
  } else if (!r1.isError && r1.text.includes("status: draft")) {
    // Memory plugin might not be built — still a valid creation
    pass("Valid workflow creates (draft — memory plugin not built, expected in test env)");
  } else {
    fail("Valid workflow creates", r1.text);
  }

  // Extract workflow ID for later tests
  const idMatch = r1.text.match(/ID: ([0-9a-f-]+)/);
  const validWorkflowId = idMatch ? idMatch[1] : null;

  // Test 2: Missing plugin creates as draft
  const r2 = await callTool("workflow-create", {
    name: "Test Draft Workflow",
    description: "A workflow referencing a non-existent plugin",
    steps: [
      {
        type: "tool",
        plugin: "nonexistent-plugin-xyz",
        tool: "fake-tool",
        params: {},
        label: "Fake step",
      },
    ],
  });

  if (!r2.isError && r2.text.includes("status: draft")) {
    pass("Missing plugin creates with status=draft");
  } else {
    fail("Missing plugin creates with status=draft", r2.text);
  }

  // Test 3: Cycle detection rejects circular deps
  const r3 = await callTool("workflow-create", {
    name: "Test Cycle Workflow",
    description: "Should fail — circular dependency",
    steps: [
      {
        type: "tool",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Step A",
        dependsOn: ["1"],
      },
      {
        type: "tool",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Step B",
        dependsOn: ["0"],
      },
    ],
  });

  if (r3.isError && r3.text.toLowerCase().includes("cycle")) {
    pass("Cycle detection rejects circular deps");
  } else {
    fail("Cycle detection rejects circular deps", r3.text);
  }

  // Test 4: Step limit (50) enforced — create 51 steps
  // The Zod schema enforces max(50), so this should fail at the schema level
  const manySteps = Array.from({ length: 51 }, (_, i) => ({
    type: "tool",
    plugin: "memory",
    tool: "memory-search",
    params: {},
    label: `Step ${i}`,
  }));

  const r4 = await callTool("workflow-create", {
    name: "Test Step Limit",
    description: "Should fail — too many steps",
    steps: manySteps,
  });

  if (r4.isError) {
    pass("Step limit (50) enforced");
  } else {
    fail("Step limit (50) enforced", "Expected error for 51 steps");
  }

  return validWorkflowId;
}

async function testTypeEnforcement() {
  console.log("\n=== Type Enforcement Tests ===");

  // Test 5: Reject type "skill"
  const r5 = await callTool("workflow-create", {
    name: "Test Skill Type",
    description: "Should fail — skill type not supported",
    steps: [
      {
        type: "skill",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Skill step",
      },
    ],
  });

  if (r5.isError) {
    pass('Type "skill" rejected by schema');
  } else {
    fail('Type "skill" rejected by schema', r5.text);
  }

  // Test 6: Reject type "subagent"
  const r6 = await callTool("workflow-create", {
    name: "Test Subagent Type",
    description: "Should fail — subagent type not supported",
    steps: [
      {
        type: "subagent",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Subagent step",
      },
    ],
  });

  if (r6.isError) {
    pass('Type "subagent" rejected by schema');
  } else {
    fail('Type "subagent" rejected by schema', r6.text);
  }
}

async function testTimeoutValidation() {
  console.log("\n=== Timeout Validation Tests ===");

  // Test 7: Reject timeout: 0
  const r7 = await callTool("workflow-create", {
    name: "Test Timeout Zero",
    description: "Should fail — timeout 0 below min",
    steps: [
      {
        type: "tool",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Zero timeout step",
        timeout: 0,
      },
    ],
  });

  if (r7.isError) {
    pass("Timeout 0 rejected (below min 1)");
  } else {
    fail("Timeout 0 rejected (below min 1)", r7.text);
  }

  // Test 8: Reject timeout: 121
  const r8 = await callTool("workflow-create", {
    name: "Test Timeout Over Max",
    description: "Should fail — timeout 121 above max",
    steps: [
      {
        type: "tool",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Over max timeout step",
        timeout: 121,
      },
    ],
  });

  if (r8.isError) {
    pass("Timeout 121 rejected (above max 120)");
  } else {
    fail("Timeout 121 rejected (above max 120)", r8.text);
  }

  // Test 9: Accept timeout: 120 (boundary)
  const r9 = await callTool("workflow-create", {
    name: "Test Timeout Boundary",
    description: "Should succeed — timeout 120 is max allowed",
    steps: [
      {
        type: "tool",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Max timeout step",
        timeout: 120,
      },
    ],
  });

  if (!r9.isError) {
    pass("Timeout 120 accepted (boundary)");
  } else {
    fail("Timeout 120 accepted (boundary)", r9.text);
  }

  // Test 10: Accept no timeout (uses default 30s)
  const r10 = await callTool("workflow-create", {
    name: "Test Timeout Default",
    description: "Should succeed — no timeout means default 30s",
    steps: [
      {
        type: "tool",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Default timeout step",
      },
    ],
  });

  if (!r10.isError) {
    pass("No timeout accepted (default 30s)");
  } else {
    fail("No timeout accepted (default 30s)", r10.text);
  }
}

async function testGetAndList(validWorkflowId) {
  console.log("\n=== Get / List Tests ===");

  // Test 11: workflow-get returns workflow details
  if (validWorkflowId) {
    const r11 = await callTool("workflow-get", { workflowId: validWorkflowId });
    if (!r11.isError && r11.text.includes("Test Valid Workflow")) {
      pass("workflow-get returns workflow details");
    } else {
      fail("workflow-get returns workflow details", r11.text);
    }
  } else {
    fail("workflow-get returns workflow details", "No valid workflow ID from creation test");
  }

  // Test 12: workflow-get for non-existent ID returns error
  const r12 = await callTool("workflow-get", { workflowId: "00000000-0000-0000-0000-000000000000" });
  if (r12.isError || r12.text.toLowerCase().includes("not found")) {
    pass("workflow-get for non-existent ID returns error");
  } else {
    fail("workflow-get for non-existent ID returns error", r12.text);
  }

  // Test 13: workflow-list returns workflows
  const r13 = await callTool("workflow-list", {});
  if (!r13.isError && r13.text.includes("workflow(s)")) {
    pass("workflow-list returns workflows");
  } else {
    fail("workflow-list returns workflows", r13.text);
  }

  // Test 14: workflow-list with status filter
  const r14 = await callTool("workflow-list", { status: "draft" });
  if (!r14.isError && (r14.text.includes("DRAFT") || r14.text.includes("No workflows"))) {
    pass("workflow-list with status filter works");
  } else {
    fail("workflow-list with status filter works", r14.text);
  }
}

async function testUpdate(validWorkflowId) {
  console.log("\n=== Update Tests ===");

  if (!validWorkflowId) {
    fail("Update tests", "No valid workflow ID");
    return;
  }

  // Test 15: Update name
  const r15 = await callTool("workflow-update", {
    workflowId: validWorkflowId,
    name: "Updated Workflow Name",
  });

  if (!r15.isError) {
    pass("Update name succeeds");
  } else {
    fail("Update name succeeds", r15.text);
  }

  // Test 16: Verify name was updated
  const r16 = await callTool("workflow-get", { workflowId: validWorkflowId });
  if (!r16.isError && r16.text.includes("Updated Workflow Name")) {
    pass("Name update persisted");
  } else {
    fail("Name update persisted", r16.text);
  }

  // Test 17: Update with no fields returns error
  const r17 = await callTool("workflow-update", {
    workflowId: validWorkflowId,
  });
  if (r17.isError) {
    pass("Update with no fields returns error");
  } else {
    fail("Update with no fields returns error", r17.text);
  }

  // Test 18: Update with invalid step type rejected
  const r18 = await callTool("workflow-update", {
    workflowId: validWorkflowId,
    steps: [
      {
        type: "skill",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Invalid type step",
      },
    ],
  });
  if (r18.isError) {
    pass("Update with invalid step type rejected");
  } else {
    fail("Update with invalid step type rejected", r18.text);
  }
}

async function testExecution() {
  console.log("\n=== Execution Tests ===");

  // Test 19: Execute on draft workflow rejected
  const draftResult = await callTool("workflow-create", {
    name: "Test Draft Execute",
    description: "Draft workflow for execution test",
    steps: [
      {
        type: "tool",
        plugin: "nonexistent-plugin-xyz",
        tool: "fake-tool",
        params: {},
        label: "Fake step",
      },
    ],
  });

  const draftIdMatch = draftResult.text.match(/ID: ([0-9a-f-]+)/);
  const draftId = draftIdMatch ? draftIdMatch[1] : null;

  if (draftId) {
    const r19 = await callTool("workflow-execute", { workflowId: draftId });
    if (r19.isError && r19.text.includes("draft")) {
      pass("Execute on draft workflow rejected");
    } else {
      fail("Execute on draft workflow rejected", r19.text);
    }
  } else {
    fail("Execute on draft workflow rejected", "Could not create draft workflow");
  }

  // Test 20: Execute on paused workflow rejected
  if (draftId) {
    // First create a valid workflow, then pause it
    const validResult = await callTool("workflow-create", {
      name: "Test Paused Execute",
      description: "Will be paused for execution test",
      steps: [
        {
          type: "tool",
          plugin: "memory",
          tool: "memory-search",
          params: { query: "test" },
          label: "Search step",
        },
      ],
    });

    const validIdMatch = validResult.text.match(/ID: ([0-9a-f-]+)/);
    const pauseId = validIdMatch ? validIdMatch[1] : null;

    if (pauseId) {
      // Pause it (only works if active — may be draft depending on env)
      await callTool("workflow-update", {
        workflowId: pauseId,
        status: "paused",
      });

      const r20 = await callTool("workflow-execute", { workflowId: pauseId });
      if (r20.isError && r20.text.includes("paused")) {
        pass("Execute on paused workflow rejected");
      } else if (r20.isError && r20.text.includes("draft")) {
        pass("Execute on non-active workflow rejected (draft in test env)");
      } else {
        fail("Execute on paused workflow rejected", r20.text);
      }
    } else {
      fail("Execute on paused workflow rejected", "Could not create workflow to pause");
    }
  }

  // Test 21: Execute non-existent workflow rejected
  const r21 = await callTool("workflow-execute", {
    workflowId: "00000000-0000-0000-0000-000000000000",
  });
  if (r21.isError && r21.text.toLowerCase().includes("not found")) {
    pass("Execute non-existent workflow rejected");
  } else {
    fail("Execute non-existent workflow rejected", r21.text);
  }
}

async function testValidation() {
  console.log("\n=== Validation Tests ===");

  // Test 22: Validate a draft workflow (missing plugin) stays draft
  const draftResult = await callTool("workflow-create", {
    name: "Test Validate Draft",
    description: "Draft for validation test",
    steps: [
      {
        type: "tool",
        plugin: "nonexistent-validate-test",
        tool: "fake-tool",
        params: {},
        label: "Missing plugin step",
      },
    ],
  });

  const draftIdMatch = draftResult.text.match(/ID: ([0-9a-f-]+)/);
  const validateId = draftIdMatch ? draftIdMatch[1] : null;

  if (validateId) {
    const r22 = await callTool("workflow-validate", { workflowId: validateId });
    if (!r22.isError && r22.text.includes("draft")) {
      pass("Validate draft workflow stays draft (missing plugin)");
    } else {
      fail("Validate draft workflow stays draft", r22.text);
    }
  } else {
    fail("Validate draft workflow stays draft", "Could not create draft workflow");
  }

  // Test 23: Validate non-existent workflow returns error
  const r23 = await callTool("workflow-validate", {
    workflowId: "00000000-0000-0000-0000-000000000000",
  });
  if (r23.isError && r23.text.toLowerCase().includes("not found")) {
    pass("Validate non-existent workflow returns error");
  } else {
    fail("Validate non-existent workflow returns error", r23.text);
  }
}

async function testDelete() {
  console.log("\n=== Delete Tests ===");

  // Test 24: Create and delete a workflow
  const createResult = await callTool("workflow-create", {
    name: "Test Delete Workflow",
    description: "Will be deleted",
    steps: [
      {
        type: "tool",
        plugin: "memory",
        tool: "memory-search",
        params: {},
        label: "Temporary step",
      },
    ],
  });

  const deleteIdMatch = createResult.text.match(/ID: ([0-9a-f-]+)/);
  const deleteId = deleteIdMatch ? deleteIdMatch[1] : null;

  if (deleteId) {
    const r24 = await callTool("workflow-delete", { workflowId: deleteId });
    if (!r24.isError && r24.text.includes("deleted")) {
      pass("Delete workflow succeeds");
    } else {
      fail("Delete workflow succeeds", r24.text);
    }

    // Test 25: Verify deleted workflow is gone
    const r25 = await callTool("workflow-get", { workflowId: deleteId });
    if (r25.isError || r25.text.toLowerCase().includes("not found")) {
      pass("Deleted workflow no longer retrievable");
    } else {
      fail("Deleted workflow no longer retrievable", r25.text);
    }
  } else {
    fail("Delete workflow succeeds", "Could not create workflow to delete");
    fail("Deleted workflow no longer retrievable", "No workflow to verify");
  }

  // Test 26: Delete non-existent workflow returns error
  const r26 = await callTool("workflow-delete", {
    workflowId: "00000000-0000-0000-0000-000000000000",
  });
  if (r26.isError && r26.text.toLowerCase().includes("not found")) {
    pass("Delete non-existent workflow returns error");
  } else {
    fail("Delete non-existent workflow returns error", r26.text);
  }
}

async function testRunHistory() {
  console.log("\n=== Run History Tests ===");

  // Test 27: workflow-runs returns empty when no runs exist
  const r27 = await callTool("workflow-runs", {});
  if (!r27.isError && (r27.text.includes("No workflow runs") || r27.text.includes("run(s)"))) {
    pass("workflow-runs returns results (empty or populated)");
  } else {
    fail("workflow-runs returns results", r27.text);
  }

  // Test 28: workflow-runs with status filter
  const r28 = await callTool("workflow-runs", { status: "completed" });
  if (!r28.isError) {
    pass("workflow-runs with status filter works");
  } else {
    fail("workflow-runs with status filter works", r28.text);
  }
}

// ─────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────

async function main() {
  console.log("=== Workflow MCP Tool Tests ===");
  console.log("");

  try {
    await setup();
  } catch (err) {
    console.error(`Setup failed: ${err.message}`);
    process.exit(1);
  }

  try {
    const validWorkflowId = await testCreation();
    await testTypeEnforcement();
    await testTimeoutValidation();
    await testGetAndList(validWorkflowId);
    await testUpdate(validWorkflowId);
    await testExecution();
    await testValidation();
    await testDelete();
    await testRunHistory();
  } catch (err) {
    console.error(`\nUnexpected error: ${err.message}`);
    failed++;
  }

  console.log("");
  await teardown();

  console.log("=== Results ===");
  console.log(`  ${passed} passed, ${failed} failed, ${passed + failed} total`);

  if (failures.length > 0) {
    console.log("\nFailures:");
    for (const f of failures) {
      console.log(`  - ${f.name}: ${f.reason}`);
    }
  }

  process.exit(failed > 0 ? 1 : 0);
}

main();
