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

vi.mock('./agent-error-detection.js', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('./agent-error-detection.js')>();
  return {
    ...actual,
    classifyRotationTrigger: vi.fn(() => ({
      shouldRetry: false,
      reason: '',
    })),
  };
});

vi.mock('./codex-token-rotation.js', () => ({
  detectCodexRotationTrigger: vi.fn(() => ({
    shouldRotate: false,
    reason: '',
  })),
}));

import { classifyRotationTrigger } from './agent-error-detection.js';
import { detectCodexRotationTrigger } from './codex-token-rotation.js';
import {
  isRetryableClaudeSessionFailureAttempt,
  resolveAttemptRetryAction,
  resolveClaudeRetryTrigger,
  resolveCodexRetryTrigger,
  resolvePairedFollowUpQueueAction,
} from './message-agent-executor-rules.js';

describe('message-agent-executor-rules', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('resolves a Claude retry trigger from streamed trigger state', () => {
    expect(
      resolveClaudeRetryTrigger({
        canRetryClaudeCredentials: true,
        provider: 'claude',
        attempt: {
          sawOutput: false,
          streamedTriggerReason: { reason: 'auth-expired', retryAfterMs: 5000 },
        },
      }),
    ).toEqual({
      reason: 'auth-expired',
      retryAfterMs: 5000,
    });
  });

  it('returns null for Claude retry when output was already visible', () => {
    expect(
      resolveClaudeRetryTrigger({
        canRetryClaudeCredentials: true,
        provider: 'claude',
        attempt: { sawOutput: true },
        fallbackMessage: '429 rate limit',
      }),
    ).toBeNull();
    expect(classifyRotationTrigger).not.toHaveBeenCalled();
  });

  it('resolves a Codex retry trigger from the codex detector', () => {
    vi.mocked(detectCodexRotationTrigger).mockReturnValue({
      shouldRotate: true,
      reason: 'auth-expired',
    });

    expect(
      resolveCodexRetryTrigger({
        canRetryCodex: true,
        attempt: {},
        rotationMessage: '401 oauth token has expired',
      }),
    ).toEqual({ reason: 'auth-expired' });
  });

  it('ignores streamed reasons that are not valid Codex rotation triggers', () => {
    expect(
      resolveCodexRetryTrigger({
        canRetryCodex: true,
        attempt: {
          streamedTriggerReason: { reason: 'usage-exhausted' },
        },
      }),
    ).toBeNull();
    expect(detectCodexRotationTrigger).not.toHaveBeenCalled();
  });

  it('resolves a shared Claude retry action from fallback output text', () => {
    vi.mocked(classifyRotationTrigger).mockReturnValue({
      shouldRetry: true,
      reason: '429',
      retryAfterMs: 30000,
    });

    expect(
      resolveAttemptRetryAction({
        provider: 'claude',
        canRetryClaudeCredentials: true,
        canRetryCodex: false,
        attempt: { sawOutput: false },
        rotationMessage: '429 rate limit',
      }),
    ).toEqual({
      kind: 'claude',
      trigger: { reason: '429', retryAfterMs: 30000 },
      rotationMessage: '429 rate limit',
    });
  });

  it('resolves a shared Codex retry action from streamed state', () => {
    expect(
      resolveAttemptRetryAction({
        provider: 'codex',
        canRetryClaudeCredentials: false,
        canRetryCodex: true,
        attempt: {
          sawOutput: false,
          streamedTriggerReason: { reason: 'auth-expired' },
        },
        rotationMessage: 'oauth token expired',
      }),
    ).toEqual({
      kind: 'codex',
      trigger: { reason: 'auth-expired' },
      rotationMessage: 'oauth token expired',
    });
  });

  it('detects retryable Claude session failures from either flag or classifier', () => {
    expect(
      isRetryableClaudeSessionFailureAttempt({
        attempt: {
          sawOutput: false,
          retryableSessionFailureDetected: true,
        },
        isClaudeCodeAgent: true,
        provider: 'claude',
        shouldRetryFreshSessionOnAgentFailure: vi.fn(() => false),
      }),
    ).toBe(true);

    const shouldRetryFreshSessionOnAgentFailure = vi.fn(() => true);
    expect(
      isRetryableClaudeSessionFailureAttempt({
        attempt: {
          sawOutput: false,
          error: new Error('stale session'),
        },
        isClaudeCodeAgent: true,
        provider: 'claude',
        shouldRetryFreshSessionOnAgentFailure,
      }),
    ).toBe(true);
    expect(shouldRetryFreshSessionOnAgentFailure).toHaveBeenCalledWith({
      result: null,
      error: 'stale session',
    });
  });

  it('resolves pending reviewer follow-up requeue from task state', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'reviewer',
        executionStatus: 'failed',
        sawOutput: false,
        taskStatus: 'review_ready',
      }),
    ).toBe('pending');
  });

  it('requeues owner follow-up after a silent arbiter Codex account failure returns the task active', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'arbiter',
        executionStatus: 'failed',
        sawOutput: false,
        taskStatus: 'active',
        outputSummary:
          'Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.\nExecution completed without a visible terminal verdict.',
      }),
    ).toBe('pending');
  });

  it('resolves pending arbiter follow-up requeue after repeated owner execution failures', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'owner',
        executionStatus: 'failed',
        sawOutput: false,
        taskStatus: 'arbiter_requested',
      }),
    ).toBe('pending');
  });

  it('resolves pending owner retry after a silent owner failure leaves the task active', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'owner',
        executionStatus: 'failed',
        sawOutput: false,
        taskStatus: 'active',
      }),
    ).toBe('pending');
  });

  it('requeues a silent owner failure caused by Codex pool unavailability', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'owner',
        executionStatus: 'failed',
        sawOutput: false,
        taskStatus: 'active',
        outputSummary:
          'auth-expired: All Codex rotation accounts unavailable; re-auth required before launching Codex\nExecution completed without a visible terminal verdict.',
      }),
    ).toBe('pending');
  });

  it('does not request an executor-side follow-up after successful owner output moved the task to review_ready', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'owner',
        executionStatus: 'succeeded',
        sawOutput: true,
        taskStatus: 'review_ready',
      }),
    ).toBe('none');
  });

  it('does not request an executor-side follow-up when reviewer success moved the task to merge_ready', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'reviewer',
        executionStatus: 'succeeded',
        sawOutput: true,
        taskStatus: 'merge_ready',
      }),
    ).toBe('none');
  });

  it('does not request a generic follow-up after successful reviewer output moved the task back to active', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'reviewer',
        executionStatus: 'succeeded',
        sawOutput: true,
        taskStatus: 'active',
      }),
    ).toBe('none');
  });

  it('does not request a duplicate executor-side follow-up after successful owner STEP_DONE output', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'owner',
        executionStatus: 'succeeded',
        sawOutput: true,
        taskStatus: 'active',
        outputSummary: 'STEP_DONE\nkeep going',
      }),
    ).toBe('none');
  });

  it('returns none after successful output when no next-turn action is needed', () => {
    expect(
      resolvePairedFollowUpQueueAction({
        completedRole: 'owner',
        executionStatus: 'succeeded',
        sawOutput: true,
        taskStatus: 'active',
      }),
    ).toBe('none');
  });
});
