/**
 * Tests for the marketplace flow-publish helpers.
 *
 * `computeFlowPublishMetadata` builds the marketplace-listing metadata.
 * `sanitizeFlowForPublish` strips per-installation identifiers (agentIds)
 * so a published flow is portable.
 *
 * Both are pure — easy to lock comprehensively.
 */
import { describe, test, expect } from '@jest/globals';
import {
  computeFlowPublishMetadata,
  sanitizeFlowForPublish,
} from '../flowPublish.js';

// ─── computeFlowPublishMetadata ───────────────────────────────────────

describe('computeFlowPublishMetadata', () => {
  test('counts nodes, agent nodes, and edges', () => {
    const flow = {
      schemaVersion: 2,
      nodes: [
        { id: 'in', type: 'input' },
        { id: 'a', type: 'agent', data: {} },
        { id: 'b', type: 'agent', data: {} },
        { id: 'out', type: 'output' },
      ],
      edges: [
        { source: 'in', target: 'a' },
        { source: 'a', target: 'b' },
        { source: 'b', target: 'out' },
      ],
    };
    const m = computeFlowPublishMetadata(flow);
    expect(m.nodeCount).toBe(4);
    expect(m.agentNodeCount).toBe(2);
    expect(m.edgeCount).toBe(3);
  });

  test('preserves schemaVersion (default 1 when absent)', () => {
    expect(computeFlowPublishMetadata({ schemaVersion: 2, nodes: [] }).schemaVersion).toBe(2);
    expect(computeFlowPublishMetadata({ nodes: [] }).schemaVersion).toBe(1);
  });

  test('collects distinct models from agent nodes (data.model OR data.preferredModel)', () => {
    const flow = {
      nodes: [
        { type: 'agent', data: { model: 'kimi-k2.6' } },
        { type: 'agent', data: { preferredModel: 'gpt-5' } },
        { type: 'agent', data: { model: 'kimi-k2.6' } },         // dup
        { type: 'agent', data: {} },                              // unmapped
        { type: 'input', data: { model: 'should-not-count' } },   // wrong type
      ],
      edges: [],
    };
    const m = computeFlowPublishMetadata(flow);
    expect(m.models.sort()).toEqual(['gpt-5', 'kimi-k2.6']);
  });

  test('detects pause / human-input nodes', () => {
    expect(computeFlowPublishMetadata({ nodes: [{ type: 'pause' }] }).hasPauseNode).toBe(true);
    expect(computeFlowPublishMetadata({ nodes: [{ type: 'human-input' }] }).hasPauseNode).toBe(true);
    expect(computeFlowPublishMetadata({ nodes: [{ type: 'agent' }] }).hasPauseNode).toBe(false);
  });

  test('detects trigger nodes (typed) AND scheduled nodes (cron-on-data)', () => {
    expect(computeFlowPublishMetadata({ nodes: [{ type: 'trigger' }] }).hasTriggerNode).toBe(true);
    expect(computeFlowPublishMetadata({ nodes: [{ type: 'agent', data: { cron: '0 9 * * *' } }] }).hasTriggerNode).toBe(true);
    expect(computeFlowPublishMetadata({ nodes: [{ type: 'agent', data: {} }] }).hasTriggerNode).toBe(false);
  });

  test('truncates the flow.description into a goal of ≤280 chars', () => {
    const long = 'A'.repeat(500);
    const m = computeFlowPublishMetadata({ description: long, nodes: [] });
    expect(m.goal.length).toBe(280);
    expect(m.goal.startsWith('A')).toBe(true);
  });

  test('handles missing description as empty string', () => {
    expect(computeFlowPublishMetadata({ nodes: [] }).goal).toBe('');
    expect(computeFlowPublishMetadata({ description: null, nodes: [] }).goal).toBe('');
    expect(computeFlowPublishMetadata({ description: 42, nodes: [] }).goal).toBe('');
  });

  test('null/undefined/garbage flow is handled defensively', () => {
    const m1 = computeFlowPublishMetadata(null);
    expect(m1.nodeCount).toBe(0);
    expect(m1.edgeCount).toBe(0);
    expect(m1.models).toEqual([]);
    expect(m1.hasPauseNode).toBe(false);
    expect(m1.hasTriggerNode).toBe(false);

    const m2 = computeFlowPublishMetadata('not an object');
    expect(m2.nodeCount).toBe(0);
  });

  test('empty arrays produce zero-counts (not NaN)', () => {
    const m = computeFlowPublishMetadata({ nodes: [], edges: [] });
    expect(m.nodeCount).toBe(0);
    expect(m.agentNodeCount).toBe(0);
    expect(m.edgeCount).toBe(0);
    expect(m.models).toEqual([]);
  });
});

// ─── sanitizeFlowForPublish ───────────────────────────────────────────

describe('sanitizeFlowForPublish', () => {
  test('wipes agentId on agent nodes (per-installation identifier — not portable)', () => {
    const flow = {
      nodes: [
        { id: 'a', type: 'agent', data: { agentId: 'agent-coder-rapid-1234', label: 'Researcher', instructions: 'Do thing' } },
      ],
      edges: [],
    };
    const out = sanitizeFlowForPublish(flow);
    expect(out.nodes[0].data.agentId).toBe('');
    // Label and instructions preserved — they're the value.
    expect(out.nodes[0].data.label).toBe('Researcher');
    expect(out.nodes[0].data.instructions).toBe('Do thing');
  });

  test('non-agent nodes pass through unchanged', () => {
    const flow = {
      nodes: [
        { id: 'in', type: 'input', data: { promptTemplate: '{{x}}' } },
        { id: 'out', type: 'output', data: { outputFormat: 'text' } },
      ],
      edges: [],
    };
    const out = sanitizeFlowForPublish(flow);
    expect(out.nodes[0]).toEqual({ id: 'in', type: 'input', data: { promptTemplate: '{{x}}' } });
    expect(out.nodes[1]).toEqual({ id: 'out', type: 'output', data: { outputFormat: 'text' } });
  });

  test('does NOT mutate the input flow (pure function)', () => {
    const flow = {
      nodes: [
        { id: 'a', type: 'agent', data: { agentId: 'live-id-do-not-touch' } },
      ],
      edges: [],
    };
    const before = JSON.parse(JSON.stringify(flow));
    sanitizeFlowForPublish(flow);
    expect(flow).toEqual(before);
  });

  test('preserves edges, schemaVersion, name, description verbatim', () => {
    const flow = {
      schemaVersion: 2,
      name: 'My pipeline',
      description: 'Does the thing',
      nodes: [{ id: 'a', type: 'agent', data: { agentId: 'x' } }],
      edges: [{ source: 'a', target: 'b' }],
      variables: { foo: 'bar' },
    };
    const out = sanitizeFlowForPublish(flow);
    expect(out.schemaVersion).toBe(2);
    expect(out.name).toBe('My pipeline');
    expect(out.description).toBe('Does the thing');
    expect(out.edges).toEqual([{ source: 'a', target: 'b' }]);
    expect(out.variables).toEqual({ foo: 'bar' });
  });

  test('agent node without data still gets data.agentId stamped (defensive)', () => {
    const flow = {
      nodes: [{ id: 'a', type: 'agent' }],
      edges: [],
    };
    const out = sanitizeFlowForPublish(flow);
    expect(out.nodes[0].data).toEqual({ agentId: '' });
  });

  test('null / non-object input passes through unchanged (no crash)', () => {
    expect(sanitizeFlowForPublish(null)).toBe(null);
    expect(sanitizeFlowForPublish(undefined)).toBe(undefined);
    expect(sanitizeFlowForPublish('string')).toBe('string');
  });

  test('mix of agent + non-agent: only agent nodes get sanitized', () => {
    const flow = {
      nodes: [
        { id: 'in', type: 'input' },
        { id: 'a', type: 'agent', data: { agentId: 'real-id-1' } },
        { id: 'b', type: 'agent', data: { agentId: 'real-id-2' } },
        { id: 'out', type: 'output' },
      ],
      edges: [],
    };
    const out = sanitizeFlowForPublish(flow);
    const ids = out.nodes.filter(n => n.type === 'agent').map(n => n.data.agentId);
    expect(ids).toEqual(['', '']);
    expect(out.nodes[0]).toEqual({ id: 'in', type: 'input' });
    expect(out.nodes[3]).toEqual({ id: 'out', type: 'output' });
  });
});
