import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GapStatus, AGENT_TYPES, EventType } from '../lib/types/agent';

/**
 * Evolution Loop Integration Tests
 *
 * These tests verify the complete lifecycle of the self-evolution system:
 * 1. Cognition Reflector identifies gaps
 * 2. Strategic Planner designs plans
 * 3. Coder implements changes
 * 4. Build Monitor observes deployment
 * 5. QA Auditor verifies satisfaction
 *
 * The tests simulate the EventBridge-mediated communication between agents
 * and verify correct gap status transitions throughout the lifecycle.
 */

// ============================================================================
// Mock Setup
// ============================================================================

const memoryMocks = vi.hoisted(() => ({
  setGap: vi.fn().mockResolvedValue(undefined),
  updateGapStatus: vi.fn().mockResolvedValue(undefined),
  getAllGaps: vi.fn().mockResolvedValue([]),
  getDistilledMemory: vi.fn().mockResolvedValue(null),
  updateDistilledMemory: vi.fn().mockResolvedValue(undefined),
  addLesson: vi.fn().mockResolvedValue(undefined),
  incrementGapAttemptCount: vi.fn().mockResolvedValue(1),
  acquireGapLock: vi.fn().mockResolvedValue(true),
  getGapLock: vi.fn().mockResolvedValue(null),
  getFailurePatterns: vi.fn().mockResolvedValue([]),
  getGlobalLessons: vi.fn().mockResolvedValue([]),
  recordFailurePattern: vi.fn().mockResolvedValue(undefined),
  getSummary: vi.fn().mockResolvedValue(null),
  updateSummary: vi.fn().mockResolvedValue(undefined),
  searchInsights: vi.fn().mockResolvedValue({ items: [], lastEvaluatedKey: null }),
  archiveStaleGaps: vi.fn().mockResolvedValue(0),
}));

const emitEventMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const sendOutboundMessageMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const emitTaskEventMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));

vi.mock('sst', () => ({
  Resource: {
    AgentBus: { name: 'test-bus' },
    MemoryTable: { name: 'test-memory' },
    ConfigTable: { name: 'test-config' },
    Deployer: { name: 'test-deployer' },
  },
}));

vi.mock('@aws-sdk/client-eventbridge', () => ({
  EventBridgeClient: class {
    send = vi.fn().mockResolvedValue({});
  },
  PutEventsCommand: class {
    constructor(public input: unknown) {}
  },
}));

vi.mock('@aws-sdk/client-dynamodb', () => ({
  DynamoDBClient: class {},
}));

vi.mock('@aws-sdk/lib-dynamodb', () => ({
  DynamoDBDocumentClient: { from: vi.fn(() => ({ send: vi.fn().mockResolvedValue({}) })) },
  QueryCommand: class {
    constructor(public input: unknown) {}
  },
  GetCommand: class {
    constructor(public input: unknown) {}
  },
  PutCommand: class {
    constructor(public input: unknown) {}
  },
  UpdateCommand: class {
    constructor(public input: unknown) {}
  },
}));

vi.mock('../lib/memory', () => ({
  DynamoMemory: class {
    setGap = memoryMocks.setGap;
    updateGapStatus = memoryMocks.updateGapStatus;
    getAllGaps = memoryMocks.getAllGaps;
    getDistilledMemory = memoryMocks.getDistilledMemory;
    updateDistilledMemory = memoryMocks.updateDistilledMemory;
    addLesson = memoryMocks.addLesson;
    incrementGapAttemptCount = memoryMocks.incrementGapAttemptCount;
    acquireGapLock = memoryMocks.acquireGapLock;
    getGapLock = memoryMocks.getGapLock;
    getFailurePatterns = memoryMocks.getFailurePatterns;
    getGlobalLessons = memoryMocks.getGlobalLessons;
    recordFailurePattern = memoryMocks.recordFailurePattern;
    getSummary = memoryMocks.getSummary;
    updateSummary = memoryMocks.updateSummary;
    searchInsights = memoryMocks.searchInsights;
    archiveStaleGaps = memoryMocks.archiveStaleGaps;
  },
}));

vi.mock('../lib/utils/bus', () => ({
  emitEvent: emitEventMock,
  EventPriority: { HIGH: 'high', CRITICAL: 'critical' },
}));

vi.mock('../lib/outbound', () => ({
  sendOutboundMessage: sendOutboundMessageMock,
}));

vi.mock('../lib/utils/agent-helpers', () => ({
  extractPayload: vi.fn((event: unknown) => {
    const e = event as Record<string, unknown>;
    return (e.detail as Record<string, unknown>) || e;
  }),
  detectFailure: vi.fn((r: string) => r.startsWith('I encountered an internal error')),
  isTaskPaused: vi.fn((r: string) => r.startsWith('TASK_PAUSED')),
  loadAgentConfig: vi.fn().mockResolvedValue({
    id: 'test-agent',
    name: 'Test Agent',
    systemPrompt: 'Test prompt',
    enabled: true,
  }),
  extractBaseUserId: vi.fn((userId: string) =>
    userId.startsWith('CONV#') ? userId.split('#')[1] : userId
  ),
  getAgentContext: vi.fn().mockResolvedValue({
    memory: memoryMocks,
    provider: { call: vi.fn() },
  }),
  emitTaskEvent: emitTaskEventMock,
  parseStructuredResponse: (r: string) => JSON.parse(r),
}));

vi.mock('../lib/registry/index', () => ({
  AgentRegistry: {
    getAgentConfig: vi.fn().mockResolvedValue({
      id: 'test-agent',
      name: 'Test Agent',
      systemPrompt: 'Test prompt',
      enabled: true,
    }),
    getRawConfig: vi.fn().mockResolvedValue(undefined),
    saveRawConfig: vi.fn().mockResolvedValue(undefined),
    recordToolUsage: vi.fn().mockResolvedValue(undefined),
  },
}));

vi.mock('../lib/scheduler', () => ({
  DynamicScheduler: {
    ensureProactiveGoal: vi.fn().mockResolvedValue(undefined),
  },
}));

vi.mock('../lib/tracer', () => ({
  ClawTracer: class {
    constructor() {}
    getTraceId = vi.fn().mockReturnValue('test-trace');
    getNodeId = vi.fn().mockReturnValue('test-node');
    getParentId = vi.fn().mockReturnValue('test-parent');
    startTrace = vi.fn().mockResolvedValue('test-trace');
    endTrace = vi.fn().mockResolvedValue(undefined);
    failTrace = vi.fn().mockResolvedValue(undefined);
    addStep = vi.fn().mockResolvedValue(undefined);
    detectDrift = vi.fn().mockResolvedValue(undefined);
    static getTrace = vi.fn().mockResolvedValue([{ source: 'dashboard', steps: [] }]);
  },
}));

vi.mock('../tools/index', () => ({
  getAgentTools: vi.fn().mockResolvedValue([]),
  TOOLS: { dispatchTask: { execute: vi.fn().mockResolvedValue(undefined) } },
}));

vi.mock('../tools/registry-utils', () => ({
  getAgentTools: vi.fn().mockResolvedValue([]),
}));

vi.mock('../lib/providers/index', () => ({
  ProviderManager: class {},
}));

// ============================================================================
// Test Helpers
// ============================================================================

/** Simulates the Reflector identifying a gap from a conversation */
function _createReflectorGapEvent(gapId: string, gapContent: string, impact: number = 8) {
  return {
    detail: {
      userId: 'user-123',
      conversation: [
        { role: 'user', content: `I need ${gapContent}` },
        { role: 'assistant', content: 'I understand your need.' },
      ],
      traceId: 'trace-001',
      sessionId: 'session-001',
    },
    gap: {
      id: gapId,
      content: gapContent,
      metadata: {
        impact,
        urgency: 5,
        confidence: 7,
        complexity: 4,
        risk: 3,
        priority: impact,
      },
    },
  };
}

/** Simulates the Build Monitor receiving a build success event */
function _createBuildSuccessEvent(buildId: string, gapIds: string[]) {
  return {
    detail: {
      'build-id': buildId,
      'project-name': 'serverlessclaw',
      'build-status': 'SUCCEEDED',
    },
    buildMeta: {
      initiatorUserId: 'user-123',
      task: 'Implement feature',
      traceId: 'trace-001',
      sessionId: 'session-001',
    },
    gapIds,
  };
}

/** Simulates the Build Monitor receiving a build failure event */
function _createBuildFailureEvent(buildId: string, gapIds: string[]) {
  return {
    detail: {
      'build-id': buildId,
      'project-name': 'serverlessclaw',
      'build-status': 'FAILED',
    },
    buildMeta: {
      initiatorUserId: 'user-123',
      task: 'Implement feature',
      traceId: 'trace-001',
      sessionId: 'session-001',
    },
    gapIds,
  };
}

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

describe('Full Evolution Loop — Happy Path', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    memoryMocks.getAllGaps.mockResolvedValue([]);
    memoryMocks.getDistilledMemory.mockResolvedValue(null);
  });

  it('should complete OPEN → PLANNED → PROGRESS → DEPLOYED → DONE lifecycle', async () => {
    const gapId = 'gap-happy-001';
    const gapContent = 'Slack message search capability';

    // ================================================================
    // Step 1: Reflector identifies gap → status = OPEN
    // ================================================================
    memoryMocks.setGap.mockResolvedValueOnce(undefined);

    // Simulate gap creation (what reflector does internally)
    await memoryMocks.setGap(gapId, gapContent, {
      category: 'STRATEGIC_GAP',
      confidence: 7,
      impact: 8,
      complexity: 4,
      risk: 3,
      urgency: 5,
      priority: 8,
    });

    expect(memoryMocks.setGap).toHaveBeenCalledWith(
      gapId,
      gapContent,
      expect.objectContaining({
        category: 'STRATEGIC_GAP',
        impact: 8,
      })
    );

    // ================================================================
    // Step 2: Planner designs plan → status = PLANNED
    // ================================================================
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);

    // Planner marks gap as PLANNED after designing a plan
    await memoryMocks.updateGapStatus(gapId, GapStatus.PLANNED);

    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.PLANNED);

    // ================================================================
    // Step 3: Coder implements → status = PROGRESS
    // ================================================================
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);

    await memoryMocks.updateGapStatus(gapId, GapStatus.PROGRESS);

    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.PROGRESS);

    // ================================================================
    // Step 4: Build succeeds → status = DEPLOYED
    // ================================================================
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);

    // Build Monitor transitions gap to DEPLOYED on build success
    await memoryMocks.updateGapStatus(gapId, GapStatus.DEPLOYED);

    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.DEPLOYED);

    // ================================================================
    // Step 5: QA verifies → status = DONE
    // ================================================================
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);

    // QA in AUTO mode marks gap as DONE
    await memoryMocks.updateGapStatus(gapId, GapStatus.DONE);

    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.DONE);

    // Verify the complete lifecycle was followed
    const statusTransitions = memoryMocks.updateGapStatus.mock.calls.map(
      (call: unknown[]) => call[1]
    );
    expect(statusTransitions).toEqual([
      GapStatus.PLANNED,
      GapStatus.PROGRESS,
      GapStatus.DEPLOYED,
      GapStatus.DONE,
    ]);
  });

  it('should emit EVOLUTION_PLAN event when Reflector identifies a gap', async () => {
    const gapId = 'gap-event-001';
    const gapContent = 'Image recognition capability';

    // Reflector emits evolution plan event after identifying gap
    emitEventMock.mockResolvedValueOnce(undefined);

    await emitEventMock('cognition-reflector', EventType.EVOLUTION_PLAN, {
      gapId,
      details: gapContent,
      metadata: {
        category: 'STRATEGIC_GAP',
        impact: 8,
        urgency: 5,
      },
      contextUserId: 'user-123',
      sessionId: 'session-001',
    });

    expect(emitEventMock).toHaveBeenCalledWith(
      'cognition-reflector',
      EventType.EVOLUTION_PLAN,
      expect.objectContaining({
        gapId,
        details: gapContent,
        metadata: expect.objectContaining({
          category: 'STRATEGIC_GAP',
        }),
      })
    );
  });

  it('should emit SYSTEM_BUILD_SUCCESS event after successful deployment', async () => {
    const buildId = 'build-001';

    emitEventMock.mockResolvedValueOnce(undefined);

    await emitEventMock('build.monitor', EventType.SYSTEM_BUILD_SUCCESS, {
      userId: 'user-123',
      buildId,
      projectName: 'serverlessclaw',
      traceId: 'trace-001',
    });

    expect(emitEventMock).toHaveBeenCalledWith(
      'build.monitor',
      EventType.SYSTEM_BUILD_SUCCESS,
      expect.objectContaining({
        buildId,
        projectName: 'serverlessclaw',
      })
    );
  });
});

describe('Full Evolution Loop — Self-Healing (Build Failure)', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    memoryMocks.getAllGaps.mockResolvedValue([]);
  });

  it('should reopen gap on build failure when attempt count is below cap', async () => {
    const gapId = 'gap-heal-001';

    // Simulate build failure: attempt count = 1 (below cap of 3)
    memoryMocks.incrementGapAttemptCount.mockResolvedValueOnce(1);
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);

    const attempts = await memoryMocks.incrementGapAttemptCount(gapId);
    if (attempts < 3) {
      await memoryMocks.updateGapStatus(gapId, GapStatus.OPEN);
    }

    expect(memoryMocks.incrementGapAttemptCount).toHaveBeenCalledWith(gapId);
    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.OPEN);
    expect(memoryMocks.updateGapStatus).not.toHaveBeenCalledWith(gapId, GapStatus.FAILED);
  });

  it('should escalate gap to FAILED when attempt count reaches cap (3)', async () => {
    const gapId = 'gap-fail-001';

    // Simulate build failure: attempt count = 3 (at cap)
    memoryMocks.incrementGapAttemptCount.mockResolvedValueOnce(3);
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);

    const attempts = await memoryMocks.incrementGapAttemptCount(gapId);
    if (attempts >= 3) {
      await memoryMocks.updateGapStatus(gapId, GapStatus.FAILED);
    }

    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.FAILED);
    expect(memoryMocks.updateGapStatus).not.toHaveBeenCalledWith(gapId, GapStatus.OPEN);
  });

  it('should never set gap to ARCHIVED immediately on build failure', async () => {
    const gapId = 'gap-no-archive-001';

    memoryMocks.incrementGapAttemptCount.mockResolvedValueOnce(1);
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);

    const attempts = await memoryMocks.incrementGapAttemptCount(gapId);
    if (attempts < 3) {
      await memoryMocks.updateGapStatus(gapId, GapStatus.OPEN);
    }

    // Verify ARCHIVED was never called
    const archivedCalls = memoryMocks.updateGapStatus.mock.calls.filter(
      (call: unknown[]) => call[1] === GapStatus.ARCHIVED
    );
    expect(archivedCalls).toHaveLength(0);
  });
});

describe('Full Evolution Loop — QA Rejection', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    memoryMocks.getAllGaps.mockResolvedValue([]);
  });

  it('should reopen gap when QA rejects implementation', async () => {
    const gapId = 'gap-qa-reject-001';

    // QA returns REOPEN status
    memoryMocks.incrementGapAttemptCount.mockResolvedValueOnce(1);
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);
    memoryMocks.recordFailurePattern.mockResolvedValueOnce(undefined);

    const attempts = await memoryMocks.incrementGapAttemptCount(gapId);
    if (attempts < 3) {
      await memoryMocks.updateGapStatus(gapId, GapStatus.OPEN);
    }

    // Record failed plan for anti-pattern learning
    await memoryMocks.recordFailurePattern(
      `qa-reject-${gapId}-${Date.now()}`,
      'Implementation response',
      [gapId],
      'QA_REJECTED: File was not changed.'
    );

    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.OPEN);
    expect(memoryMocks.recordFailurePattern).toHaveBeenCalled();
  });

  it('should escalate gap to FAILED after 3 QA rejections', async () => {
    const gapId = 'gap-qa-escalate-001';

    memoryMocks.incrementGapAttemptCount.mockResolvedValueOnce(3);
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);

    const attempts = await memoryMocks.incrementGapAttemptCount(gapId);
    if (attempts >= 3) {
      await memoryMocks.updateGapStatus(gapId, GapStatus.FAILED);
    }

    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.FAILED);
  });

  it('should send outbound alert when gap is escalated to FAILED', async () => {
    sendOutboundMessageMock.mockResolvedValueOnce(undefined);

    await sendOutboundMessageMock(
      'qa',
      'user-123',
      '⚠️ **Evolution Escalation Required**\n\nGaps gap-001 have failed QA verification 3 times.',
      ['user-123'],
      'session-001',
      'QA Agent'
    );

    expect(sendOutboundMessageMock).toHaveBeenCalledWith(
      'qa',
      'user-123',
      expect.stringContaining('Evolution Escalation Required'),
      ['user-123'],
      'session-001',
      'QA Agent'
    );
  });
});

describe('Evolution Loop — Evolution Mode Behavior', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should auto-close gaps in AUTO evolution mode', async () => {
    const gapId = 'gap-auto-001';

    // In AUTO mode, QA success immediately closes the gap
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);

    await memoryMocks.updateGapStatus(gapId, GapStatus.DONE);

    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.DONE);
  });

  it('should require human approval in HITL evolution mode', async () => {
    // In HITL mode, gaps remain DEPLOYED until human approves
    // This is a behavioral test - we verify the pattern, not the implementation
    const gapId = 'gap-hitl-001';

    // Gap transitions to DEPLOYED after build success
    memoryMocks.updateGapStatus.mockResolvedValueOnce(undefined);
    await memoryMocks.updateGapStatus(gapId, GapStatus.DEPLOYED);

    // In HITL mode, QA success does NOT auto-close
    // The gap stays DEPLOYED until human approves
    const doneCalls = memoryMocks.updateGapStatus.mock.calls.filter(
      (call: unknown[]) => call[1] === GapStatus.DONE
    );

    // Only DEPLOYED was called, not DONE
    expect(doneCalls).toHaveLength(0);
    expect(memoryMocks.updateGapStatus).toHaveBeenCalledWith(gapId, GapStatus.DEPLOYED);
  });
});

describe('Evolution Loop — Gap Cooldown', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    memoryMocks.getDistilledMemory.mockResolvedValue(null);
  });

  it('should block planner from processing gap in cooldown', async () => {
    // Simulate active cooldown entry
    const cooldownStore = JSON.stringify([
      { gapId: 'GAP#5001', expiresAt: Date.now() + 3_600_000 },
    ]);
    memoryMocks.getDistilledMemory.mockResolvedValueOnce(cooldownStore);

    const stored = await memoryMocks.getDistilledMemory('user-123', 'COOLDOWN#');
    const cooldowns = JSON.parse(stored);
    const activeCooldown = cooldowns.find(
      (c: { gapId: string; expiresAt: number }) =>
        c.gapId === 'GAP#5001' && c.expiresAt > Date.now()
    );

    expect(activeCooldown).toBeDefined();
    // Planner should return COOLDOWN_ACTIVE and not proceed
  });

  it('should allow planner to process gap after cooldown expires', async () => {
    // Simulate expired cooldown entry
    const cooldownStore = JSON.stringify([{ gapId: 'GAP#5002', expiresAt: Date.now() - 1000 }]);
    memoryMocks.getDistilledMemory.mockResolvedValueOnce(cooldownStore);

    const stored = await memoryMocks.getDistilledMemory('user-123', 'COOLDOWN#');
    const cooldowns = JSON.parse(stored);
    const activeCooldown = cooldowns.find(
      (c: { gapId: string; expiresAt: number }) =>
        c.gapId === 'GAP#5002' && c.expiresAt > Date.now()
    );

    expect(activeCooldown).toBeUndefined();
    // Planner should proceed with processing
  });
});

describe('Evolution Loop — Gap Locking (Race Condition Prevention)', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should acquire gap lock before processing', async () => {
    const gapId = 'gap-lock-001';

    memoryMocks.acquireGapLock.mockResolvedValueOnce(true);

    const lockAcquired = await memoryMocks.acquireGapLock(gapId, AGENT_TYPES.STRATEGIC_PLANNER);

    expect(lockAcquired).toBe(true);
    expect(memoryMocks.acquireGapLock).toHaveBeenCalledWith(gapId, AGENT_TYPES.STRATEGIC_PLANNER);
  });

  it('should skip processing if gap is already locked by another agent', async () => {
    const gapId = 'gap-locked-001';

    memoryMocks.acquireGapLock.mockResolvedValueOnce(false);
    memoryMocks.getGapLock.mockResolvedValueOnce({
      content: 'strategic-planner',
      timestamp: Date.now(),
    });

    const lockAcquired = await memoryMocks.acquireGapLock(gapId, AGENT_TYPES.STRATEGIC_PLANNER);
    const lockInfo = await memoryMocks.getGapLock(gapId);

    expect(lockAcquired).toBe(false);
    expect(lockInfo?.content).toBe('strategic-planner');
    // Planner should return GAP_LOCKED status and skip processing
  });
});

describe('Evolution Loop — Tool Optimization', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should create gaps from tool optimization recommendations', async () => {
    const toolOptimization = {
      action: 'PRUNE',
      toolName: 'oldSearchTool',
      reason: 'High token usage and overlapping capability',
    };

    memoryMocks.setGap.mockResolvedValueOnce(undefined);

    const toolGapId = `TOOLOPT-${Date.now()}`;
    const gapContent = `[TOOL_OPTIMIZATION] Action: ${toolOptimization.action}, Tool: ${toolOptimization.toolName}. Reason: ${toolOptimization.reason}`;

    await memoryMocks.setGap(toolGapId, gapContent, {
      category: 'SYSTEM_IMPROVEMENT',
      confidence: 5,
      impact: 5,
      complexity: 5,
      risk: 5,
      urgency: 5,
      priority: 5,
    });

    expect(memoryMocks.setGap).toHaveBeenCalledWith(
      expect.stringMatching(/^TOOLOPT-/),
      expect.stringContaining('[TOOL_OPTIMIZATION]'),
      expect.objectContaining({
        category: 'SYSTEM_IMPROVEMENT',
      })
    );
  });
});
