import { Test, TestingModule } from '@nestjs/testing';
import { AiJobExecutionEngineImpl } from './ai-job-execution-engine';
import { AiJobLifecycleRepository } from './ai-job-lifecycle.repository';
import { JobDefinitionRegistry } from './job-definition-registry';
import { AiJobStateMachine } from './ai-job-state-machine';
import { AiGatewayService } from '../ai/gateway/ai-gateway.service';
import { ProjectionExecutor } from './projection-executor.service';
import { ActiveRecallExecutor } from './active-recall-executor';
import { ActiveRecallObservabilityService } from './active-recall-observability.service';
import { FeynmanExecutor } from './feynman-executor';
import { FeynmanBusinessValidator, FeynmanReferenceValidator } from './feynman-validator';
import { FeynmanObservabilityService } from './feynman-observability.service';
import { ReviewCardGenerationExecutor } from './review-card-generation-executor';
import { ReviewCardGenerationValidator } from './review-card-generation-validator';
import { QuizGenerationExecutor } from './quiz-generation-executor';
import { QuizGenerationValidator } from './quiz-generation-validator';
import { LearningAnalysisExecutor } from './learning-analysis-executor';
import { LearningAnalysisValidator } from './learning-analysis-validator';
import { PrismaService } from '../../infrastructure/database/prisma.service';
import { JobLockConflictError, JobAlreadyTerminalError } from './ai-job.errors';

function validDef() {
  return {
    jobType: 'test_job',
    queue: { queueName: 'ai-interactive', defaultPriority: 0 },
    execution: { timeoutMs: 5000, maxRetries: 2, retryBackoff: { type: 'exponential', delay: 2000 }, cancellable: true, abortStrategy: 'fail' },
    input: { schemaVersion: '1.0' },
    output: { schemaVersion: '1.0' },
    prompt: { promptKey: 'test', promptVersion: '1.0' },
    model: { modelTier: 'primary', modelProvider: 'deepseek', modelName: 'deepseek-chat' },
    credential: { allowedModes: ['platform_key'], defaultMode: 'platform_key' },
    security: { contentSafetyCheck: false, outputRedaction: false },
    metadata: { label: 'Test', description: '', domain: 'analysis', version: '1.0' },
  };
}

describe('AiJobExecutionEngineImpl', () => {
  let engine: AiJobExecutionEngineImpl;
  let prisma: any;
  let lifecycleRepo: any;
  let registry: any;
  let stateMachine: any;
  let aiGateway: any;
  let projectionExecutor: any;

  function makeJob(overrides?: any) {
    return {
      id: 'job-001',
      userId: 'user-1',
      jobType: 'test_job',
      lifecycleStatus: 'queued',
      queueName: 'ai-interactive',
      attemptCount: 0,
      cancelRequestedAt: null,
      ...overrides,
    };
  }

  beforeEach(async () => {
    lifecycleRepo = {
      lockJob: jest.fn(),
      markSucceeded: jest.fn(),
      markFailed: jest.fn(),
      markCancelled: jest.fn(),
    };
    registry = { get: jest.fn().mockReturnValue(validDef()) };
    stateMachine = new AiJobStateMachine();
    aiGateway = { generate: jest.fn() };
    projectionExecutor = { execute: jest.fn().mockResolvedValue([]) };

    prisma = {
      aiJob: {
        findUnique: jest.fn(),
        update: jest.fn(),
      },
      aiJobSnapshot: { findUnique: jest.fn() },
      aiUsageLog: { create: jest.fn() },
    };

    jest.spyOn(require('@nestjs/common').Logger.prototype, 'log').mockImplementation(() => {});
    jest.spyOn(require('@nestjs/common').Logger.prototype, 'warn').mockImplementation(() => {});
    jest.spyOn(require('@nestjs/common').Logger.prototype, 'error').mockImplementation(() => {});

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AiJobExecutionEngineImpl,
        { provide: PrismaService, useValue: prisma },
        { provide: AiJobLifecycleRepository, useValue: lifecycleRepo },
        { provide: JobDefinitionRegistry, useValue: registry },
        { provide: AiJobStateMachine, useValue: stateMachine },
        { provide: AiGatewayService, useValue: aiGateway },
        { provide: ProjectionExecutor, useValue: projectionExecutor },
        { provide: ActiveRecallExecutor, useValue: { execute: jest.fn() } },
        { provide: FeynmanExecutor, useValue: { execute: jest.fn() } },
        { provide: FeynmanBusinessValidator, useValue: { validate: jest.fn() } },
        { provide: FeynmanReferenceValidator, useValue: { validate: jest.fn() } },
        { provide: ReviewCardGenerationExecutor, useValue: { execute: jest.fn() } },
        { provide: ReviewCardGenerationValidator, useValue: { validate: jest.fn() } },
        { provide: QuizGenerationExecutor, useValue: { execute: jest.fn() } },
        { provide: QuizGenerationValidator, useValue: { validate: jest.fn() } },
        { provide: LearningAnalysisExecutor, useValue: { execute: jest.fn() } },
        { provide: LearningAnalysisValidator, useValue: { validate: jest.fn() } },
        { provide: ActiveRecallObservabilityService, useValue: {
          incrementUnifiedExecuteSuccess: jest.fn(),
          incrementUnifiedExecuteFailed: jest.fn(),
          incrementUnifiedRetry: jest.fn(),
          incrementProjectorFailed: jest.fn(),
          logExecutionCompleted: jest.fn(),
          logExecutionFailed: jest.fn(),
          logRollback: jest.fn(),
        } },
        { provide: FeynmanObservabilityService, useValue: {
          incrementUnifiedExecuteSuccess: jest.fn(),
          incrementUnifiedExecuteFailed: jest.fn(),
          incrementUnifiedRetry: jest.fn(),
          incrementProjectorFailed: jest.fn(),
          addFocusItemCreated: jest.fn(),
          addReviewCardCreated: jest.fn(),
          logExecutionCompleted: jest.fn(),
          logExecutionFailed: jest.fn(),
          logRollback: jest.fn(),
        } },
      ],
    }).compile();

    engine = module.get(AiJobExecutionEngineImpl);
  });

  const ctx = {
    jobId: 'bull-001',
    attemptMade: 1,
    updateProgress: jest.fn(),
  };

  describe('正常执行全链路', () => {
    it('PREPARE→RESOLVE→EXECUTE→PROJECT→COMPLETE 全部成功', async () => {
      prisma.aiJob.findUnique.mockResolvedValue(makeJob());
      lifecycleRepo.lockJob.mockResolvedValue(makeJob({ lifecycleStatus: 'running' }));
      prisma.aiJobSnapshot.findUnique.mockResolvedValue({
        schemaVersion: '1.0',
        content: { snapshotData: true },
      });
      aiGateway.generate.mockResolvedValue({
        parsed: { result: 'ok' },
        usage: { provider: 'deepseek', model: 'deepseek-chat', inputTokens: 100, outputTokens: 50, estimatedCost: 0.001, latencyMs: 800 },
      });

      await engine.execute('job-001', ctx);

      // PREPARE: lockJob called
      expect(lifecycleRepo.lockJob).toHaveBeenCalledWith('job-001', 'bull-001');
      // EXECUTE: AiGateway called
      expect(aiGateway.generate).toHaveBeenCalled();
      // PROJECT: validatedOutput + outputHash written
      expect(prisma.aiJob.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: 'job-001' },
          data: expect.objectContaining({
            validatedOutput: { result: 'ok' },
            outputHash: expect.any(String),
          }),
        }),
      );
      // PROJECT: ProjectionExecutor called
      expect(projectionExecutor.execute).toHaveBeenCalledWith(
        undefined, // no projectorKey in validDef
        expect.objectContaining({
          job: expect.objectContaining({ id: 'job-001' }),
          validatedOutput: { result: 'ok' },
        }),
      );
      // COMPLETE: usage log
      expect(prisma.aiUsageLog.create).toHaveBeenCalled();
    });
  });

  describe('终态跳过', () => {
    it('Job 已 succeeded → 跳过执行', async () => {
      prisma.aiJob.findUnique.mockResolvedValue(makeJob({ lifecycleStatus: 'succeeded' }));

      await engine.execute('job-001', ctx);
      expect(lifecycleRepo.lockJob).not.toHaveBeenCalled();
      expect(aiGateway.generate).not.toHaveBeenCalled();
    });
  });

  describe('取消检查', () => {
    it('queued + cancelRequestedAt → 直接标记 cancelled', async () => {
      prisma.aiJob.findUnique.mockResolvedValue(makeJob({ cancelRequestedAt: new Date() }));

      await engine.execute('job-001', ctx);
      expect(lifecycleRepo.markCancelled).toHaveBeenCalledWith('job-001');
    });

    it('执行中被取消 → cancelled，不创建 Artifact', async () => {
      prisma.aiJob.findUnique
        .mockResolvedValueOnce(makeJob()) // initial read
        .mockResolvedValueOnce(makeJob({ cancelRequestedAt: new Date() })); // after lockJob, check
      lifecycleRepo.lockJob.mockResolvedValue(makeJob({ lifecycleStatus: 'running' }));

      await engine.execute('job-001', ctx);
      expect(lifecycleRepo.markCancelled).toHaveBeenCalled();
      // Provider 不应被调用
      expect(aiGateway.generate).not.toHaveBeenCalled();
    });
  });

  describe('错误分类', () => {
    it('429 → retryable → 抛给 BullMQ', async () => {
      prisma.aiJob.findUnique.mockResolvedValue(makeJob());
      lifecycleRepo.lockJob.mockResolvedValue(makeJob({ lifecycleStatus: 'running' }));
      prisma.aiJobSnapshot.findUnique.mockResolvedValue({ schemaVersion: '1.0', content: {} });

      aiGateway.generate.mockRejectedValue(new Error('429 rate limited'));

      await expect(engine.execute('job-001', ctx)).rejects.toThrow('429');
      // 不调用 markFailed（让 BullMQ 重试）
      expect(lifecycleRepo.markFailed).not.toHaveBeenCalled();
    });

    it('schema validation → permanent → markFailed（不抛错，BullMQ 不再重试）', async () => {
      prisma.aiJob.findUnique.mockResolvedValue(makeJob());
      lifecycleRepo.lockJob.mockResolvedValue(makeJob({ lifecycleStatus: 'running', attemptCount: 1 }));
      prisma.aiJobSnapshot.findUnique.mockResolvedValue({ schemaVersion: '1.0', content: {} });

      aiGateway.generate.mockRejectedValue(new Error('schema validation failed'));

      // 永久错误：不应抛出（避免 BullMQ retry）
      await engine.execute('job-001', ctx);
      expect(lifecycleRepo.markFailed).toHaveBeenCalledWith(
        'job-001',
        expect.objectContaining({ errorCode: 'schema_validation_failed' }),
      );
    });

    it('timeout → retryable → 抛给 BullMQ', async () => {
      prisma.aiJob.findUnique.mockResolvedValue(makeJob());
      lifecycleRepo.lockJob.mockResolvedValue(makeJob({ lifecycleStatus: 'running' }));
      prisma.aiJobSnapshot.findUnique.mockResolvedValue({ schemaVersion: '1.0', content: {} });

      aiGateway.generate.mockRejectedValue(new Error('timeout ETIMEDOUT'));

      await expect(engine.execute('job-001', ctx)).rejects.toThrow();
      expect(lifecycleRepo.markFailed).not.toHaveBeenCalled();
    });
  });

  describe('抢锁冲突', () => {
    it('已被其他 Worker 抢走 → 静默退出', async () => {
      prisma.aiJob.findUnique.mockResolvedValue(makeJob());
      lifecycleRepo.lockJob.mockRejectedValue(new JobLockConflictError('job-001'));

      await engine.execute('job-001', ctx);
      // 不应抛错，静默退出
      expect(aiGateway.generate).not.toHaveBeenCalled();
    });
  });

  describe('Snapshot schema 不兼容', () => {
    it('schemaVersion 不匹配 → markFailed', async () => {
      prisma.aiJob.findUnique.mockResolvedValue(makeJob());
      lifecycleRepo.lockJob.mockResolvedValue(makeJob({ lifecycleStatus: 'running' }));
      prisma.aiJobSnapshot.findUnique.mockResolvedValue({
        schemaVersion: '99.0', // incompatible
        content: {},
      });

      await engine.execute('job-001', ctx);
      expect(lifecycleRepo.markFailed).toHaveBeenCalledWith(
        'job-001',
        expect.objectContaining({ errorCode: 'schema_validation_failed' }),
      );
      expect(aiGateway.generate).not.toHaveBeenCalled();
    });
  });

  describe('Usage Logging', () => {
    it('每次 Provider 调用写 AiUsageLog', async () => {
      prisma.aiJob.findUnique.mockResolvedValue(makeJob());
      lifecycleRepo.lockJob.mockResolvedValue(makeJob({ lifecycleStatus: 'running', attemptCount: 0 }));
      prisma.aiJobSnapshot.findUnique.mockResolvedValue({ schemaVersion: '1.0', content: {} });
      aiGateway.generate.mockResolvedValue({
        parsed: { ok: true },
        usage: { provider: 'deepseek', model: 'deepseek-chat', inputTokens: 50, outputTokens: 20, estimatedCost: 0.0005, latencyMs: 300 },
      });

      await engine.execute('job-001', ctx);

      expect(prisma.aiUsageLog.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            userId: 'user-1',
            jobId: 'job-001',
            provider: 'deepseek',
            model: 'deepseek-chat',
            inputTokens: 50,
            outputTokens: 20,
          }),
        }),
      );
    });
  });
});
