import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {StepExecutionStatus, WorkflowExecutionStatus, WorkflowStepType, WorkflowTriggerType} from '@plunk/db';
import {WorkflowExecutionService} from '../WorkflowExecutionService';
import {factories, getPrismaClient} from '../../../../../test/helpers';

/**
 * Tests for WEBHOOK step config templating.
 *
 * `executeWebhook` is a private static method but is invokable at runtime
 * through a `as any` cast. We mock `safeFetch` (also private) via the
 * same mechanism so we can capture the rendered request without making a
 * real network call.
 */
describe('WorkflowExecutionService.executeWebhook templating', () => {
  let projectId: string;
  const prisma = getPrismaClient();

  // Capture (url, options) passed to safeFetch
  let safeFetchSpy: ReturnType<typeof vi.spyOn>;
  let captured: {url: string; options: RequestInit} | null = null;

  beforeEach(async () => {
    const {project} = await factories.createUserWithProject();
    projectId = project.id;

    captured = null;
    safeFetchSpy = vi
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      .spyOn(WorkflowExecutionService as any, 'safeFetch')
      .mockImplementation(async (...args: unknown[]) => {
        const [url, options] = args as [string, RequestInit];
        captured = {url, options};
        return new Response('{"ok":true}', {
          status: 200,
          headers: {'Content-Type': 'application/json'},
        });
      });
  });

  afterEach(() => {
    safeFetchSpy.mockRestore();
  });

  /**
   * Helper: build a workflow with a single WEBHOOK step using the given
   * config, plus a contact and a RUNNING execution. Returns the args
   * shape `executeWebhook` expects.
   */
  async function setup(
    webhookConfig: Record<string, unknown>,
    contactOverrides: {data?: Record<string, unknown>} = {},
    executionContext: Record<string, unknown> = {},
  ) {
    const contact = await factories.createContact({
      projectId,
      data: contactOverrides.data,
    });

    const workflow = await factories.createWorkflow({
      projectId,
      enabled: true,
      triggerType: WorkflowTriggerType.EVENT,
      triggerConfig: {eventName: 'test.event'},
    });

    const step = await prisma.workflowStep.create({
      data: {
        workflowId: workflow.id,
        type: WorkflowStepType.WEBHOOK,
        name: 'Webhook',
        position: {x: 0, y: 0},
        config: webhookConfig,
      },
    });

    const execution = await prisma.workflowExecution.create({
      data: {
        workflowId: workflow.id,
        contactId: contact.id,
        status: WorkflowExecutionStatus.RUNNING,
        context: executionContext,
      },
      include: {contact: true, workflow: true},
    });

    const stepExecution = await prisma.workflowStepExecution.create({
      data: {
        executionId: execution.id,
        stepId: step.id,
        status: StepExecutionStatus.RUNNING,
        startedAt: new Date(),
      },
    });

    return {step, execution, stepExecution};
  }

  async function invokeWebhook(
    step: unknown,
    execution: unknown,
    stepExecution: unknown,
    config: unknown,
  ) {
    // Call through `as any` because executeWebhook is private at the
    // TypeScript level. JS has no actual access control.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return (WorkflowExecutionService as any).executeWebhook(step, execution, stepExecution, config);
  }

  it('renders {{vars}} in the URL from contact.data', async () => {
    const {step, execution, stepExecution} = await setup(
      {
        url: 'https://example.com/api/users/{{userId}}',
        method: 'GET',
      },
      {data: {userId: 'abc-123'}},
    );

    await invokeWebhook(step, execution, stepExecution, step.config);

    expect(captured).not.toBeNull();
    expect(captured!.url).toBe('https://example.com/api/users/abc-123');
  });

  it('renders {{vars}} in header values', async () => {
    const {step, execution, stepExecution} = await setup(
      {
        url: 'https://example.com/hook',
        method: 'POST',
        headers: {
          Authorization: 'Bearer {{apiToken}}',
          'X-Static': 'literal',
        },
      },
      {data: {apiToken: 'secret-token-xyz'}},
    );

    await invokeWebhook(step, execution, stepExecution, step.config);

    expect(captured).not.toBeNull();
    const headers = captured!.options.headers as Record<string, string>;
    expect(headers.Authorization).toBe('Bearer secret-token-xyz');
    expect(headers['X-Static']).toBe('literal');
  });

  it('renders {{vars}} in nested object body leaves and JSON-encodes', async () => {
    const {step, execution, stepExecution} = await setup(
      {
        url: 'https://example.com/hook',
        method: 'POST',
        body: {
          user: {
            email: '{{email}}',
            name: '{{firstName}}',
          },
          ref: 'literal-ref',
          tags: ['plan:{{plan}}', 'static'],
        },
      },
      {data: {firstName: 'Ada', plan: 'gold'}},
      {campaignId: 'camp-9'},
    );

    await invokeWebhook(step, execution, stepExecution, step.config);

    expect(captured).not.toBeNull();
    const body = JSON.parse(captured!.options.body as string);
    expect(body.user.email).toBe(execution.contact.email);
    expect(body.user.name).toBe('Ada');
    expect(body.ref).toBe('literal-ref');
    expect(body.tags).toEqual(['plan:gold', 'static']);
  });

  it('leaves non-string body leaves untouched', async () => {
    const {step, execution, stepExecution} = await setup({
      url: 'https://example.com/hook',
      method: 'POST',
      body: {
        score: 42,
        active: true,
        deleted: null,
        meta: {
          count: 7,
          enabled: false,
        },
        tags: ['{{plan ?? free}}', 100, false],
      },
    });

    await invokeWebhook(step, execution, stepExecution, step.config);

    expect(captured).not.toBeNull();
    const body = JSON.parse(captured!.options.body as string);
    expect(body.score).toBe(42);
    expect(body.active).toBe(true);
    expect(body.deleted).toBe(null);
    expect(body.meta).toEqual({count: 7, enabled: false});
    // String leaf rendered (with default), non-string leaves preserved.
    expect(body.tags).toEqual(['free', 100, false]);
  });

  it('renders {{event.*}} variables from the trigger payload', async () => {
    const {step, execution, stepExecution} = await setup(
      {
        url: 'https://example.com/hooks/{{event.referrer}}',
        method: 'POST',
        headers: {
          'X-Email-Id': '{{event.emailId}}',
        },
        body: {
          referrer: '{{event.referrer}}',
          subject: '{{event.subject}}',
        },
      },
      {},
      {referrer: 'newsletter-may', emailId: 'eml_abc123', subject: 'Welcome'},
    );

    await invokeWebhook(step, execution, stepExecution, step.config);

    expect(captured).not.toBeNull();
    expect(captured!.url).toBe('https://example.com/hooks/newsletter-may');
    const headers = captured!.options.headers as Record<string, string>;
    expect(headers['X-Email-Id']).toBe('eml_abc123');
    const body = JSON.parse(captured!.options.body as string);
    expect(body.referrer).toBe('newsletter-may');
    expect(body.subject).toBe('Welcome');
  });
});
