import type { FlowEdge, FlowNode } from './types';
import { validateFlow } from './validate-flow';

function node(
  nodeKey: string,
  type: FlowNode['type'],
  label: string,
  config: Record<string, unknown> = {},
): FlowNode {
  return {
    nodeKey,
    type,
    label,
    positionX: 0,
    positionY: 0,
    config,
    status: 'valid',
  };
}

function edge(
  edgeKey: string,
  sourceNodeKey: string,
  targetNodeKey: string,
  condition?: string,
): FlowEdge {
  return {
    edgeKey,
    sourceNodeKey,
    targetNodeKey,
    condition,
  };
}

const validRecoveryNodes = [
  node('trigger_1', 'TRIGGER', 'Pagamento falhou', {
    trigger: 'payment.failed',
    source: 'stripe',
  }),
  node('wait_1', 'WAIT', 'Esperar 15 minutos', {
    waitType: 'duration',
    duration: 15,
    unit: 'minutes',
  }),
  node('action_1', 'ACTION', 'Enviar e-mail', {
    channel: 'EMAIL',
    body: 'Olá {{nome}}, atualize seu cartão: {{link_pagamento}}',
  }),
  node('wait_2', 'WAIT', 'Esperar 24 horas', {
    waitType: 'duration',
    duration: 24,
    unit: 'hours',
  }),
  node('cond_1', 'CONDITION', 'Pagamento foi recuperado?', {
    operator: 'equals',
    value: 'paid',
  }),
  node('end_ok', 'END', 'Marcar como convertido', { outcome: 'converted' }),
  node('action_2', 'ACTION', 'Enviar WhatsApp', {
    channel: 'WHATSAPP',
    body: 'Oi {{nome}}, seu pagamento falhou.',
  }),
  node('end_pending', 'END', 'Encerrar com pendência', {
    outcome: 'pending_payment',
  }),
] satisfies FlowNode[];

const validRecoveryEdges = [
  edge('e1', 'trigger_1', 'wait_1'),
  edge('e2', 'wait_1', 'action_1'),
  edge('e3', 'action_1', 'wait_2'),
  edge('e4', 'wait_2', 'cond_1'),
  edge('e5', 'cond_1', 'end_ok', 'true'),
  edge('e6', 'cond_1', 'action_2', 'false'),
  edge('e7', 'action_2', 'end_pending'),
] satisfies FlowEdge[];

describe('validateFlow', () => {
  it('accepts the payment recovery reference flow', () => {
    expect(validateFlow(validRecoveryNodes, validRecoveryEdges)).toEqual({
      valid: true,
      issues: [],
    });
  });

  it('returns NO_TRIGGER when there is no trigger', () => {
    const result = validateFlow(
      validRecoveryNodes.slice(1),
      validRecoveryEdges,
    );
    expect(result.valid).toBe(false);
    expect(result.issues).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ code: 'NO_TRIGGER', severity: 'error' }),
      ]),
    );
  });

  it('returns ORPHAN_NODE when a node is disconnected', () => {
    const result = validateFlow(
      [
        ...validRecoveryNodes,
        node('action_orphan', 'ACTION', 'Enviar push', {
          channel: 'PUSH',
          body: 'Oi',
        }),
      ],
      validRecoveryEdges,
    );
    expect(result.issues).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          nodeKey: 'action_orphan',
          code: 'ORPHAN_NODE',
        }),
      ]),
    );
  });

  it('returns CONDITION_MISSING_FALSE_PATH when a condition has no false edge', () => {
    const result = validateFlow(
      validRecoveryNodes,
      validRecoveryEdges.filter((item) => item.condition !== 'false'),
    );
    expect(result.issues).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          nodeKey: 'cond_1',
          code: 'CONDITION_MISSING_FALSE_PATH',
        }),
      ]),
    );
  });

  it('returns WAIT_MISSING_DURATION when a wait duration is missing', () => {
    const nodes = validRecoveryNodes.map((item) =>
      item.nodeKey === 'wait_1'
        ? { ...item, config: { waitType: 'duration', unit: 'minutes' } }
        : item,
    );
    const result = validateFlow(nodes, validRecoveryEdges);
    expect(result.issues).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          nodeKey: 'wait_1',
          code: 'WAIT_MISSING_DURATION',
        }),
      ]),
    );
  });

  it('returns INFINITE_LOOP as an error when a cycle has no wait or end breaker', () => {
    const result = validateFlow(
      [
        node('trigger_1', 'TRIGGER', 'Lead criado', {
          trigger: 'lead.created',
        }),
        node('action_1', 'ACTION', 'Marcar interesse', { tag: 'quente' }),
        node('action_2', 'ACTION', 'Atualizar funil', { stage: 'lead' }),
      ],
      [
        edge('e1', 'trigger_1', 'action_1'),
        edge('e2', 'action_1', 'action_2'),
        edge('e3', 'action_2', 'action_1'),
      ],
    );
    expect(result.valid).toBe(false);
    expect(result.issues).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          code: 'INFINITE_LOOP',
          severity: 'error',
        }),
      ]),
    );
  });

  it('does not emit INFINITE_LOOP when a cycle contains a wait breaker', () => {
    const result = validateFlow(
      [
        node('trigger_1', 'TRIGGER', 'Lead criado', {
          trigger: 'lead.created',
        }),
        node('action_1', 'ACTION', 'Enviar e-mail', {
          channel: 'EMAIL',
          body: 'Oi {{nome}}',
        }),
        node('wait_1', 'WAIT', 'Esperar 1 dia', {
          waitType: 'duration',
          duration: 1,
          unit: 'days',
        }),
      ],
      [
        edge('e1', 'trigger_1', 'action_1'),
        edge('e2', 'action_1', 'wait_1'),
        edge('e3', 'wait_1', 'action_1'),
      ],
    );
    expect(result.issues).not.toEqual(
      expect.arrayContaining([
        expect.objectContaining({ code: 'INFINITE_LOOP' }),
      ]),
    );
  });

  it('returns SPLIT_NOT_100 when a percentage branch does not total 100', () => {
    const branch = node('branch_1', 'BRANCH', 'Distribuição percentual', {
      distribution: [
        { path: 'A', percentage: 60 },
        { path: 'B', percentage: 30 },
      ],
    });
    const result = validateFlow(
      [
        node('trigger_1', 'TRIGGER', 'Lead criado', {
          trigger: 'lead.created',
        }),
        branch,
        node('end_a', 'END', 'Encerrar A'),
        node('end_b', 'END', 'Encerrar B'),
      ],
      [
        edge('e1', 'trigger_1', 'branch_1'),
        edge('e2', 'branch_1', 'end_a', 'A'),
        edge('e3', 'branch_1', 'end_b', 'B'),
      ],
    );
    expect(result.issues).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ nodeKey: 'branch_1', code: 'SPLIT_NOT_100' }),
      ]),
    );
  });
});
