/**
 * @fileoverview V2 Action Adapter unit tests
 * @description Tests for adaptActionHandlerToNodeDefinition
 *
 * Coverage:
 * - varsPatch generation (set/delete)
 * - nextLabel mapping
 * - Error code mapping
 * - Tab/frame state vars
 * - paused/control directive handling
 * - Output capture
 */

import { describe, expect, it, vi } from 'vitest';

import type {
  ActionExecutionContext,
  ActionExecutionResult,
  ActionErrorCode,
  ActionHandler,
} from '@/entrypoints/background/record-replay/actions/types';
import type {
  NodeExecutionContext,
  NodeExecutionResult,
} from '@/entrypoints/background/record-replay-v3/engine/plugins/types';
import type { FlowV3 } from '@/entrypoints/background/record-replay-v3/domain/flow';
import type { RunId, NodeId } from '@/entrypoints/background/record-replay-v3/domain/ids';
import { RR_ERROR_CODES } from '@/entrypoints/background/record-replay-v3/domain/errors';
import { FLOW_SCHEMA_VERSION } from '@/entrypoints/background/record-replay-v3/domain/flow';

import { adaptActionHandlerToNodeDefinition } from '@/entrypoints/background/record-replay-v3/engine/plugins/action-handler-node-adapter';

// ==================== Test Fixtures ====================

function createMockV3Context(overrides: Partial<NodeExecutionContext> = {}): NodeExecutionContext {
  const flow: FlowV3 = {
    schemaVersion: FLOW_SCHEMA_VERSION,
    id: 'test-flow',
    name: 'Test Flow',
    createdAt: new Date(0).toISOString(),
    updatedAt: new Date(0).toISOString(),
    entryNodeId: 'node-1',
    nodes: [],
    edges: [],
  };

  return {
    runId: 'run-1' as RunId,
    flow,
    nodeId: 'node-1' as NodeId,
    tabId: 1,
    vars: {},
    log: vi.fn(),
    chooseNext: (label: string) => ({ kind: 'edgeLabel' as const, label }),
    artifacts: {
      screenshot: vi.fn().mockResolvedValue({ ok: true, base64: 'mock-base64' }),
    },
    persistent: {
      get: vi.fn().mockResolvedValue(undefined),
      set: vi.fn().mockResolvedValue(undefined),
      delete: vi.fn().mockResolvedValue(undefined),
    },
    ...overrides,
  };
}

function createMockNode(id = 'node-1', config: Record<string, unknown> = {}) {
  return {
    id: id as NodeId,
    kind: 'test' as const,
    config,
  };
}

type TestActionType = 'script';

function createMockHandler(
  runFn: (
    ctx: ActionExecutionContext,
    action: unknown,
  ) => Promise<ActionExecutionResult<TestActionType>>,
): ActionHandler<TestActionType> {
  return {
    type: 'script' as TestActionType,
    run: runFn,
  };
}

function assertSucceeded(result: NodeExecutionResult): Extract<NodeExecutionResult, { status: 'succeeded' }> {
  expect(result.status).toBe('succeeded');
  if (result.status !== 'succeeded') {
    throw new Error('Expected succeeded NodeExecutionResult');
  }
  return result;
}

function assertFailed(result: NodeExecutionResult): Extract<NodeExecutionResult, { status: 'failed' }> {
  expect(result.status).toBe('failed');
  if (result.status !== 'failed') {
    throw new Error('Expected failed NodeExecutionResult');
  }
  return result;
}

// ==================== Tests ====================

describe('adaptActionHandlerToNodeDefinition', () => {
  describe('Basic execution', () => {
    it('returns succeeded for successful V2 handler', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      expect(result.status).toBe('succeeded');
    });

    it('maps V2 failed status to V3 failed', async () => {
      const handler = createMockHandler(async () => ({
        status: 'failed',
        error: { code: 'TIMEOUT', message: 'Timed out' },
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const failed = assertFailed(result);
      expect(failed.error.code).toBe(RR_ERROR_CODES.TIMEOUT);
      expect(failed.error.message).toBe('Timed out');
    });

    it('handles V2 handler throwing exception', async () => {
      const handler = createMockHandler(async () => {
        throw new Error('Unexpected error');
      });

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const failed = assertFailed(result);
      expect(failed.error.code).toBe(RR_ERROR_CODES.INTERNAL);
      expect(failed.error.message).toContain('Unexpected error');
    });
  });

  describe('varsPatch generation', () => {
    it('generates set patch for new variable', async () => {
      const handler = createMockHandler(async (ctx) => {
        ctx.vars['newVar'] = 'value';
        return { status: 'success' };
      });

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context({ vars: {} });
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.varsPatch).toContainEqual({ op: 'set', name: 'newVar', value: 'value' });
    });

    it('generates set patch for modified variable', async () => {
      const handler = createMockHandler(async (ctx) => {
        ctx.vars['existing'] = 'modified';
        return { status: 'success' };
      });

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context({ vars: { existing: 'original' } });
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.varsPatch).toContainEqual({ op: 'set', name: 'existing', value: 'modified' });
    });

    it('generates delete patch for removed variable', async () => {
      const handler = createMockHandler(async (ctx) => {
        delete ctx.vars['toDelete'];
        return { status: 'success' };
      });

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context({ vars: { toDelete: 'value' } });
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.varsPatch).toContainEqual({ op: 'delete', name: 'toDelete' });
    });

    it('handles deep object changes', async () => {
      const handler = createMockHandler(async (ctx) => {
        ctx.vars['obj'] = { nested: { value: 42 } };
        return { status: 'success' };
      });

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context({ vars: { obj: { nested: { value: 1 } } } });
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.varsPatch).toContainEqual({
        op: 'set',
        name: 'obj',
        value: { nested: { value: 42 } },
      });
    });

    it('does not generate patch when vars unchanged', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context({ vars: { existing: 'value' } });
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.varsPatch).toBeUndefined();
    });
  });

  describe('nextLabel mapping', () => {
    it('maps nextLabel to chooseNext result', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
        nextLabel: 'true',
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.next).toEqual({ kind: 'edgeLabel', label: 'true' });
    });

    it('does not set next when no nextLabel', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.next).toBeUndefined();
    });
  });

  describe('Error code mapping', () => {
    const errorCodes: Array<{ v2Code: ActionErrorCode; v3Code: string }> = [
      { v2Code: 'VALIDATION_ERROR', v3Code: RR_ERROR_CODES.VALIDATION_ERROR },
      { v2Code: 'TIMEOUT', v3Code: RR_ERROR_CODES.TIMEOUT },
      { v2Code: 'TAB_NOT_FOUND', v3Code: RR_ERROR_CODES.TAB_NOT_FOUND },
      { v2Code: 'FRAME_NOT_FOUND', v3Code: RR_ERROR_CODES.FRAME_NOT_FOUND },
      { v2Code: 'TARGET_NOT_FOUND', v3Code: RR_ERROR_CODES.TARGET_NOT_FOUND },
      { v2Code: 'ELEMENT_NOT_VISIBLE', v3Code: RR_ERROR_CODES.ELEMENT_NOT_VISIBLE },
      { v2Code: 'NAVIGATION_FAILED', v3Code: RR_ERROR_CODES.NAVIGATION_FAILED },
      { v2Code: 'NETWORK_REQUEST_FAILED', v3Code: RR_ERROR_CODES.NETWORK_REQUEST_FAILED },
      { v2Code: 'SCRIPT_FAILED', v3Code: RR_ERROR_CODES.SCRIPT_FAILED },
      { v2Code: 'DOWNLOAD_FAILED', v3Code: RR_ERROR_CODES.TOOL_ERROR },
      { v2Code: 'ASSERTION_FAILED', v3Code: RR_ERROR_CODES.TOOL_ERROR },
      { v2Code: 'UNKNOWN', v3Code: RR_ERROR_CODES.INTERNAL },
    ];

    errorCodes.forEach(({ v2Code, v3Code }) => {
      it(`maps V2 ${v2Code} to V3 ${v3Code}`, async () => {
        const handler = createMockHandler(async () => ({
          status: 'failed',
          error: { code: v2Code, message: 'Test error' },
        }));

        const nodeDef = adaptActionHandlerToNodeDefinition(handler);
        const ctx = createMockV3Context();
        const node = createMockNode();

        const result = await nodeDef.execute(ctx, node as any);

        const failed = assertFailed(result);
        expect(failed.error.code).toBe(v3Code);
      });
    });
  });

  describe('Tab/frame state vars', () => {
    it('persists newTabId as __rr_runtime__tabId', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
        newTabId: 42,
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.varsPatch).toContainEqual({
        op: 'set',
        name: '__rr_runtime__tabId',
        value: 42,
      });
    });

    it('persists ctx.frameId as __rr_runtime__frameId', async () => {
      const handler = createMockHandler(async (ctx) => {
        ctx.frameId = 5;
        return { status: 'success' };
      });

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.varsPatch).toContainEqual({
        op: 'set',
        name: '__rr_runtime__frameId',
        value: 5,
      });
    });

    it('reads tabId from __rr_runtime__tabId var', async () => {
      let capturedTabId: number | undefined;
      const handler = createMockHandler(async (ctx) => {
        capturedTabId = ctx.tabId;
        return { status: 'success' };
      });

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context({
        tabId: 1,
        vars: { __rr_runtime__tabId: 99 },
      });
      const node = createMockNode();

      await nodeDef.execute(ctx, node as any);

      expect(capturedTabId).toBe(99);
    });

    it('reads frameId from __rr_runtime__frameId var', async () => {
      let capturedFrameId: number | undefined;
      const handler = createMockHandler(async (ctx) => {
        capturedFrameId = ctx.frameId;
        return { status: 'success' };
      });

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context({
        vars: { __rr_runtime__frameId: 7 },
      });
      const node = createMockNode();

      await nodeDef.execute(ctx, node as any);

      expect(capturedFrameId).toBe(7);
    });

    it('supports custom state var names', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
        newTabId: 42,
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler, {
        stateVars: { tabIdVar: 'custom_tab', frameIdVar: 'custom_frame' },
      });
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.varsPatch).toContainEqual({
        op: 'set',
        name: 'custom_tab',
        value: 42,
      });
    });
  });

  describe('Unsupported V2 behaviors', () => {
    it('returns failed for paused status', async () => {
      const handler = createMockHandler(async () => ({
        status: 'paused',
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const failed = assertFailed(result);
      expect(failed.error.code).toBe(RR_ERROR_CODES.RUN_PAUSED);
    });

    it('returns failed for control directive (foreach)', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
        control: {
          kind: 'foreach' as const,
          listVar: 'items',
          itemVar: 'item',
          subflowId: 'subflow-1',
        },
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const failed = assertFailed(result);
      expect(failed.error.code).toBe(RR_ERROR_CODES.UNSUPPORTED_NODE);
      expect(failed.error.message).toContain('foreach');
    });

    it('returns failed for control directive (while)', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
        control: {
          kind: 'while' as const,
          condition: { kind: 'compare', left: 'a', op: 'eq', right: 'b' },
          subflowId: 'subflow-1',
          maxIterations: 10,
        },
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const failed = assertFailed(result);
      expect(failed.error.code).toBe(RR_ERROR_CODES.UNSUPPORTED_NODE);
      expect(failed.error.message).toContain('while');
    });
  });

  describe('Output capture', () => {
    it('captures output in outputs map', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
        output: { result: { extracted: 'data' } },
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode('extract-node');

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.outputs).toEqual({
        'extract-node': { result: { extracted: 'data' } },
      });
    });

    it('respects includeOutput: false option', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
        output: { result: { extracted: 'data' } },
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler, {
        includeOutput: false,
      });
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.outputs).toBeUndefined();
    });

    it('does not include outputs when no output', async () => {
      const handler = createMockHandler(async () => ({
        status: 'success',
      }));

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const succeeded = assertSucceeded(result);
      expect(succeeded.outputs).toBeUndefined();
    });
  });

  describe('Validation', () => {
    it('calls handler validate and returns error on failure', async () => {
      const handler: ActionHandler<TestActionType> = {
        type: 'script',
        validate: () => ({ ok: false, errors: ['Invalid config'] }),
        run: async () => ({ status: 'success' }),
      };

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      const failed = assertFailed(result);
      expect(failed.error.code).toBe(RR_ERROR_CODES.VALIDATION_ERROR);
      expect(failed.error.message).toContain('Invalid config');
    });

    it('proceeds with execution when validation passes', async () => {
      const handler: ActionHandler<TestActionType> = {
        type: 'script',
        validate: () => ({ ok: true }),
        run: async () => ({ status: 'success' }),
      };

      const nodeDef = adaptActionHandlerToNodeDefinition(handler);
      const ctx = createMockV3Context();
      const node = createMockNode();

      const result = await nodeDef.execute(ctx, node as any);

      expect(result.status).toBe('succeeded');
    });
  });
});
