import { describe, it, expect } from 'vitest';
import {
  resolveTemplates,
  resolveExpression,
  getNestedValue,
  evaluateConditions,
  evaluateRule,
  getSuccessorsFromNodes,
  getNextNodes,
} from '../dsl/helpers';
import type { WorkflowDSL, DslNode } from '../dsl/types';

// ─── getNestedValue ──────────────────────────────────────────────────────────

describe('getNestedValue', () => {
  it('resolves simple path', () => {
    expect(getNestedValue({ a: 1 }, 'a')).toBe(1);
  });

  it('resolves nested path', () => {
    expect(getNestedValue({ a: { b: { c: 42 } } }, 'a.b.c')).toBe(42);
  });

  it('returns undefined for missing path', () => {
    expect(getNestedValue({ a: 1 }, 'b')).toBeUndefined();
  });

  it('returns undefined for null intermediate', () => {
    expect(getNestedValue({ a: null }, 'a.b')).toBeUndefined();
  });

  it('resolves array index via dot notation', () => {
    expect(getNestedValue({ items: ['x', 'y'] }, 'items.0')).toBe('x');
  });
});

// ─── resolveTemplates ────────────────────────────────────────────────────────

describe('resolveTemplates', () => {
  const ctx = { name: 'Alice', node1: { status: 200, data: { id: 5 } } };

  it('replaces simple template', () => {
    expect(resolveTemplates('Hello {{name}}', ctx)).toBe('Hello Alice');
  });

  it('replaces nested path template', () => {
    expect(resolveTemplates('Status: {{node1.status}}', ctx)).toBe('Status: 200');
  });

  it('replaces multiple templates in one string', () => {
    expect(resolveTemplates('{{name}} got {{node1.status}}', ctx)).toBe('Alice got 200');
  });

  it('replaces missing path with empty string', () => {
    expect(resolveTemplates('{{missing}}', ctx)).toBe('');
  });

  it('recursively resolves objects', () => {
    const obj = { url: 'https://api.com/{{node1.data.id}}', method: 'GET' };
    expect(resolveTemplates(obj, ctx)).toEqual({ url: 'https://api.com/5', method: 'GET' });
  });

  it('recursively resolves arrays', () => {
    expect(resolveTemplates(['{{name}}', '{{node1.status}}'], ctx)).toEqual(['Alice', '200']);
  });

  it('passes through non-string primitives', () => {
    expect(resolveTemplates(42, ctx)).toBe(42);
    expect(resolveTemplates(null, ctx)).toBe(null);
    expect(resolveTemplates(true, ctx)).toBe(true);
  });
});

// ─── resolveExpression ───────────────────────────────────────────────────────

describe('resolveExpression', () => {
  it('resolves existing path', () => {
    expect(resolveExpression('a.b', { a: { b: 10 } })).toBe(10);
  });

  it('returns raw string for unresolvable expression', () => {
    expect(resolveExpression('literal_value', {})).toBe('literal_value');
  });
});

// ─── evaluateRule ────────────────────────────────────────────────────────────

describe('evaluateRule', () => {
  it('equals / ==', () => {
    expect(evaluateRule('hello', 'equals', 'hello')).toBe(true);
    expect(evaluateRule('hello', '==', 'world')).toBe(false);
  });

  it('notEquals / !=', () => {
    expect(evaluateRule('a', 'notEquals', 'b')).toBe(true);
    expect(evaluateRule('a', '!=', 'a')).toBe(false);
  });

  it('contains / notContains', () => {
    expect(evaluateRule('hello world', 'contains', 'world')).toBe(true);
    expect(evaluateRule('hello world', 'notContains', 'xyz')).toBe(true);
  });

  it('numeric comparisons', () => {
    expect(evaluateRule(10, 'greaterThan', '5')).toBe(true);
    expect(evaluateRule(10, '>', '15')).toBe(false);
    expect(evaluateRule(10, 'lessThan', '15')).toBe(true);
    expect(evaluateRule(10, '<', '5')).toBe(false);
    expect(evaluateRule(10, '>=', '10')).toBe(true);
    expect(evaluateRule(10, '<=', '10')).toBe(true);
  });

  it('isEmpty / isNotEmpty', () => {
    expect(evaluateRule(null, 'isEmpty', '')).toBe(true);
    expect(evaluateRule('', 'isEmpty', '')).toBe(true);
    expect(evaluateRule('x', 'isEmpty', '')).toBe(false);
    expect(evaluateRule('x', 'isNotEmpty', '')).toBe(true);
    expect(evaluateRule(null, 'isNotEmpty', '')).toBe(false);
  });

  it('unknown operator returns false', () => {
    expect(evaluateRule('a', 'unknownOp', 'b')).toBe(false);
  });
});

// ─── evaluateConditions ──────────────────────────────────────────────────────

describe('evaluateConditions', () => {
  const ctx = { status: 'active', count: '10' };

  it('returns true for empty conditions', () => {
    expect(evaluateConditions([], ctx, 'AND')).toBe(true);
  });

  it('AND: all must pass', () => {
    const conds = [
      { field: 'status', operator: 'equals', value: 'active' },
      { field: 'count', operator: 'greaterThan', value: '5' },
    ];
    expect(evaluateConditions(conds, ctx, 'AND')).toBe(true);
  });

  it('AND: one fails → false', () => {
    const conds = [
      { field: 'status', operator: 'equals', value: 'active' },
      { field: 'count', operator: 'greaterThan', value: '100' },
    ];
    expect(evaluateConditions(conds, ctx, 'AND')).toBe(false);
  });

  it('OR: one passes → true', () => {
    const conds = [
      { field: 'status', operator: 'equals', value: 'inactive' },
      { field: 'count', operator: 'equals', value: '10' },
    ];
    expect(evaluateConditions(conds, ctx, 'OR')).toBe(true);
  });

  it('OR: all fail → false', () => {
    const conds = [
      { field: 'status', operator: 'equals', value: 'inactive' },
      { field: 'count', operator: 'greaterThan', value: '100' },
    ];
    expect(evaluateConditions(conds, ctx, 'OR')).toBe(false);
  });
});

// ─── Graph Navigation ────────────────────────────────────────────────────────

describe('getSuccessorsFromNodes', () => {
  const dsl: WorkflowDSL = {
    nodes: [
      { id: 'a', type: 'manual_trigger', label: 'A', position: { x: 0, y: 0 }, config: {} },
      { id: 'b', type: 'http_request', label: 'B', position: { x: 0, y: 0 }, config: {} },
      { id: 'c', type: 'db_query', label: 'C', position: { x: 0, y: 0 }, config: {} },
    ],
    edges: [
      { id: 'e1', source: 'a', target: 'b' },
      { id: 'e2', source: 'a', target: 'c' },
      { id: 'e3', source: 'b', target: 'c' },
    ],
  };

  it('finds direct successors', () => {
    expect(getSuccessorsFromNodes(dsl, ['a'])).toEqual(['b', 'c']);
  });

  it('finds successors from multiple sources', () => {
    expect(getSuccessorsFromNodes(dsl, ['a', 'b'])).toEqual(['b', 'c', 'c']);
  });

  it('returns empty for leaf nodes', () => {
    expect(getSuccessorsFromNodes(dsl, ['c'])).toEqual([]);
  });
});

describe('getNextNodes', () => {
  const dsl: WorkflowDSL = {
    nodes: [
      { id: 'cond', type: 'if_else', label: 'Condition', position: { x: 0, y: 0 }, config: {} },
      { id: 'yes', type: 'http_request', label: 'Yes', position: { x: 0, y: 0 }, config: {} },
      { id: 'no', type: 'send_notification', label: 'No', position: { x: 0, y: 0 }, config: {} },
    ],
    edges: [
      { id: 'e1', source: 'cond', target: 'yes', sourceHandle: 'true' },
      { id: 'e2', source: 'cond', target: 'no', sourceHandle: 'false' },
    ],
  };

  it('follows branch edge when branch specified', () => {
    const node = dsl.nodes[0];
    expect(getNextNodes(dsl, node, 'true')).toEqual(['yes']);
    expect(getNextNodes(dsl, node, 'false')).toEqual(['no']);
  });

  it('follows all edges when no branch', () => {
    const node = dsl.nodes[0];
    expect(getNextNodes(dsl, node, undefined)).toEqual(['yes', 'no']);
  });

  it('falls back to all edges if branch has no matching edge', () => {
    const node = dsl.nodes[0];
    expect(getNextNodes(dsl, node, 'nonexistent')).toEqual(['yes', 'no']);
  });
});
