import { Test, TestingModule } from '@nestjs/testing';
import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
import { JobDefinitionRegistry, DuplicateJobTypeError } from './job-definition-registry';
import { ActiveRecallRegistrationService } from './active-recall-registration.service';
import { ACTIVE_RECALL_JOB_DEFINITION } from './active-recall-job-definition';
import { ActiveRecallSnapshotBuilder } from './active-recall-snapshot-builder';
import { PrismaService } from '../../infrastructure/database/prisma.service';

// ═══════════════════════════════════════════════════════════════════════════
// ActiveRecallRegistrationService
// ═══════════════════════════════════════════════════════════════════════════

describe('ActiveRecallRegistrationService', () => {
  let registry: JobDefinitionRegistry;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        JobDefinitionRegistry,
        ActiveRecallRegistrationService,
      ],
    }).compile();

    registry = module.get(JobDefinitionRegistry);
    // onModuleInit is called by NestJS during module init;
    // in tests we call it manually for verification
  });

  describe('Definition 注册', () => {
    it('Registry 注册成功', async () => {
      const module = await Test.createTestingModule({
        providers: [JobDefinitionRegistry, ActiveRecallRegistrationService],
      }).compile();
      await module.init(); // triggers onModuleInit

      const reg = module.get(JobDefinitionRegistry);
      const def = reg.get('active_recall');

      expect(def).toBeDefined();
      expect(def.jobType).toBe('active_recall');
      expect(def.queue.queueName).toBe('ai-interactive');
      expect(def.metadata.domain).toBe('analysis');
      expect(def.prompt.promptKey).toBe('active-recall-analysis');
      expect(def.prompt.promptVersion).toBe('1.0.0');
    });

    it('重复注册失败（幂等注册）', async () => {
      const module = await Test.createTestingModule({
        providers: [JobDefinitionRegistry, ActiveRecallRegistrationService],
      }).compile();
      await module.init();

      const reg = module.get(JobDefinitionRegistry);

      // 第二次注册应抛出 DuplicateJobTypeError
      expect(() => reg.register(ACTIVE_RECALL_JOB_DEFINITION)).toThrow(
        DuplicateJobTypeError,
      );
      expect(() => reg.register(ACTIVE_RECALL_JOB_DEFINITION)).toThrow(
        'Duplicate jobType "active_recall"',
      );
    });
  });

  describe('Definition 字段冻结验证', () => {
    it('jobType 格式合法', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.jobType).toMatch(/^[a-z][a-z0-9_]{1,63}$/);
    });

    it('queueName 在允许列表', () => {
      expect(['ai-interactive', 'ai-background']).toContain(
        ACTIVE_RECALL_JOB_DEFINITION.queue.queueName,
      );
    });

    it('input.schemaVersion 非空', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.input.schemaVersion).toBe('active-recall-v1');
    });

    it('output.schemaVersion 非空', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.output.schemaVersion).toBe('active-recall-v1');
    });

    it('promptKey 非空', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.prompt.promptKey.length).toBeGreaterThan(0);
    });

    it('timeoutMs 在 [1000, 600000] 范围内', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.execution.timeoutMs).toBeGreaterThanOrEqual(1000);
      expect(ACTIVE_RECALL_JOB_DEFINITION.execution.timeoutMs).toBeLessThanOrEqual(600000);
    });

    it('maxRetries 在 [0, 10] 范围内', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.execution.maxRetries).toBeGreaterThanOrEqual(0);
      expect(ACTIVE_RECALL_JOB_DEFINITION.execution.maxRetries).toBeLessThanOrEqual(10);
    });

    it('credential.allowedModes 非空且值合法', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.credential.allowedModes.length).toBeGreaterThan(0);
      for (const m of ACTIVE_RECALL_JOB_DEFINITION.credential.allowedModes) {
        expect(['platform_key', 'user_deepseek_key']).toContain(m);
      }
    });

    it('retryBackoff 合法', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.execution.retryBackoff.type).toBe('exponential');
      expect(ACTIVE_RECALL_JOB_DEFINITION.execution.retryBackoff.delay).toBeGreaterThan(0);
    });

    it('projectorKey 已设置', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.projectorKey).toBe('active_recall_projector');
    });

    it('contentSafetyCheck 已启用', () => {
      expect(ACTIVE_RECALL_JOB_DEFINITION.security.contentSafetyCheck).toBe(true);
    });
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// ActiveRecallSnapshotBuilder
// ═══════════════════════════════════════════════════════════════════════════

describe('ActiveRecallSnapshotBuilder', () => {
  let builder: ActiveRecallSnapshotBuilder;
  let prisma: any;
  let registry: any;

  const mockQuestion = {
    id: 'q-001',
    userId: 'u-001',
    knowledgeItemId: 'ki-001',
    questionText: '请解释光合作用的过程',
    difficulty: 'normal',
    createdBy: 'ai',
    createdAt: new Date('2026-06-20T10:00:00Z'),
    updatedAt: new Date('2026-06-20T10:00:00Z'),
  };

  const mockAnswer = {
    id: 'a-001',
    userId: 'u-001',
    questionId: 'q-001',
    sessionId: null,
    answerType: 'text',
    answerText: '光合作用是植物利用光能...',
    audioFileId: null,
    submittedAt: new Date('2026-06-21T08:30:00Z'),
    createdAt: new Date('2026-06-21T08:30:00Z'),
  };

  beforeEach(async () => {
    prisma = {
      activeRecallAnswer: {
        findUnique: jest.fn(),
      },
      activeRecallQuestion: {
        findUnique: jest.fn(),
      },
    };

    // Mock JobDefinitionRegistry: 返回与 ACTIVE_RECALL_JOB_DEFINITION 一致的 Definition
    registry = {
      get: jest.fn().mockReturnValue(ACTIVE_RECALL_JOB_DEFINITION),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ActiveRecallSnapshotBuilder,
        { provide: PrismaService, useValue: prisma },
        { provide: JobDefinitionRegistry, useValue: registry },
      ],
    }).compile();

    builder = module.get(ActiveRecallSnapshotBuilder);
  });

  describe('build', () => {
    it('构建有效快照', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(mockQuestion);

      const snapshot = await builder.build('u-001', 'a-001');

      expect(snapshot.schemaVersion).toBe('active-recall-v1');
      expect(snapshot.snapshot.userId).toBe('u-001');
      expect(snapshot.snapshot.activeRecallId).toBe('q-001');
      expect(snapshot.snapshot.knowledgeItemId).toBe('ki-001');
      expect(snapshot.snapshot.questionText).toBe('请解释光合作用的过程');
      expect(snapshot.snapshot.userAnswer).toBe('光合作用是植物利用光能...');
      expect(snapshot.snapshot.answerId).toBe('a-001');
      expect(snapshot.snapshot.submittedAt).toBe('2026-06-21T08:30:00.000Z');
      // 以下值从 JobDefinition 读取（单一事实来源），不硬编码
      expect(snapshot.snapshot.promptKey).toBe(ACTIVE_RECALL_JOB_DEFINITION.prompt.promptKey);
      expect(snapshot.snapshot.promptVersion).toBe(ACTIVE_RECALL_JOB_DEFINITION.prompt.promptVersion);
      expect(snapshot.snapshot.modelTier).toBe(ACTIVE_RECALL_JOB_DEFINITION.model.modelTier);
      expect(snapshot.snapshot.modelProvider).toBe(ACTIVE_RECALL_JOB_DEFINITION.model.modelProvider);
      expect(snapshot.snapshot.modelName).toBe(ACTIVE_RECALL_JOB_DEFINITION.model.modelName);
      expect(snapshot.snapshot.maxTokens).toBe(4096);
      expect(snapshot.snapshot.temperature).toBe(0.3);
    });

    it('prompt/model 值全部来自 JobDefinition（单一事实来源）', async () => {
      // 验证：修改 Definition → Snapshot 自动跟随（无重复硬编码）
      const customDef = { ...ACTIVE_RECALL_JOB_DEFINITION, prompt: { promptKey: 'custom-key', promptVersion: '2.0' } };
      registry.get.mockReturnValue(customDef);

      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(mockQuestion);

      const snapshot = await builder.build('u-001', 'a-001');
      expect(snapshot.snapshot.promptKey).toBe('custom-key');
      expect(snapshot.snapshot.promptVersion).toBe('2.0');
      // 未修改的字段保持 Definition 原值
      expect(snapshot.snapshot.modelProvider).toBe(ACTIVE_RECALL_JOB_DEFINITION.model.modelProvider);
    });

    it('非法输入被拒绝：答案不存在 → NotFoundException', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(null);

      await expect(builder.build('u-001', 'a-nonexistent')).rejects.toThrow(
        NotFoundException,
      );
      await expect(builder.build('u-001', 'a-nonexistent')).rejects.toThrow(
        'ActiveRecallAnswer a-nonexistent not found',
      );
    });

    it('非法输入被拒绝：答案不属于当前用户 → ForbiddenException', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue({
        ...mockAnswer,
        userId: 'u-other',
      });

      await expect(builder.build('u-001', 'a-001')).rejects.toThrow(
        ForbiddenException,
      );
      await expect(builder.build('u-001', 'a-001')).rejects.toThrow(
        'does not belong to user u-001',
      );
    });

    it('非法输入被拒绝：答案无关联问题 → NotFoundException', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue({
        ...mockAnswer,
        questionId: null,
      });

      await expect(builder.build('u-001', 'a-001')).rejects.toThrow(
        NotFoundException,
      );
      await expect(builder.build('u-001', 'a-001')).rejects.toThrow(
        'has no associated question',
      );
    });

    it('非法输入被拒绝：关联问题不存在 → NotFoundException', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(null);

      await expect(builder.build('u-001', 'a-001')).rejects.toThrow(
        NotFoundException,
      );
      await expect(builder.build('u-001', 'a-001')).rejects.toThrow(
        'ActiveRecallQuestion q-001 not found',
      );
    });

    it('Snapshot 不含敏感字段', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(mockQuestion);

      const snapshot = await builder.build('u-001', 'a-001');
      const serialized = JSON.stringify(snapshot);

      // 禁止字段
      expect(serialized).not.toContain('Authorization');
      expect(serialized).not.toContain('JWT');
      expect(serialized).not.toContain('apiKey');
      expect(serialized).not.toContain('api_key');
      expect(serialized).not.toContain('cookie');
      expect(serialized).not.toContain('Cookie');
      expect(serialized).not.toContain('database');
      expect(serialized).not.toContain('connectionInfo');
      expect(serialized).not.toContain('email');
      expect(serialized).not.toContain('password');
      expect(serialized).not.toContain('credential');

      // Snapshot 对象内部不应有空字段的完整 schema
      const snap = snapshot.snapshot as Record<string, unknown>;
      expect(snap).not.toHaveProperty('jwt');
      expect(snap).not.toHaveProperty('authorization');
      expect(snap).not.toHaveProperty('credentialId');
    });

    it('knowledgeItemId 为 null 时正常处理', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue({
        ...mockQuestion,
        knowledgeItemId: null,
      });

      const snapshot = await builder.build('u-001', 'a-001');
      expect(snapshot.snapshot.knowledgeItemId).toBeNull();
    });

    it('answerText 为 null 时使用空字符串', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue({
        ...mockAnswer,
        answerText: null,
      });
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(mockQuestion);

      const snapshot = await builder.build('u-001', 'a-001');
      expect(snapshot.snapshot.userAnswer).toBe('');
    });
  });

  describe('buildSnapshot (M-AI-04-05 集成入口)', () => {
    it('targetType="active_recall_answer" 正常工作', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(mockQuestion);

      const snapshot = await builder.buildSnapshot('u-001', 'active_recall_answer', 'a-001');

      expect(snapshot.schemaVersion).toBe('active-recall-v1');
      expect(snapshot.snapshot.userId).toBe('u-001');
      expect(snapshot.snapshot.answerId).toBe('a-001');
    });

    it('非法 targetType → BadRequestException', async () => {
      await expect(
        builder.buildSnapshot('u-001', 'wrong_type', 'a-001'),
      ).rejects.toThrow(BadRequestException);

      await expect(
        builder.buildSnapshot('u-001', 'wrong_type', 'a-001'),
      ).rejects.toThrow(
        "ActiveRecallSnapshotBuilder only supports targetType='active_recall_answer'",
      );
    });

    it('签名与 SnapshotBuilderService.buildSnapshot(userId, targetType, targetId) 兼容', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(mockQuestion);

      // 证明 3 参数签名可用于 AiJobCreationService 调用
      const snapshot = await builder.buildSnapshot('u-001', 'active_recall_answer', 'a-001');
      expect(snapshot).toBeDefined();
      expect(snapshot.snapshot.questionText).toBe('请解释光合作用的过程');
    });
  });

  describe('computeHash', () => {
    it('相同输入 → 相同 contentHash（稳定）', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(mockQuestion);

      const s1 = await builder.build('u-001', 'a-001');
      const s2 = await builder.build('u-001', 'a-001');

      const h1 = builder.computeHash(s1);
      const h2 = builder.computeHash(s2);

      expect(h1).toBe(h2);
      expect(h1.length).toBe(16);
    });

    it('不同输入 → 不同 contentHash', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(mockQuestion);

      const s1 = await builder.build('u-001', 'a-001');

      // 修改 answerText → 不同 hash
      prisma.activeRecallAnswer.findUnique.mockResolvedValue({
        ...mockAnswer,
        answerText: 'Different answer',
      });
      const s2 = await builder.build('u-001', 'a-001');

      const h1 = builder.computeHash(s1);
      const h2 = builder.computeHash(s2);

      expect(h1).not.toBe(h2);
    });

    it('contentHash 长度固定为 16', async () => {
      prisma.activeRecallAnswer.findUnique.mockResolvedValue(mockAnswer);
      prisma.activeRecallQuestion.findUnique.mockResolvedValue(mockQuestion);

      const snapshot = await builder.build('u-001', 'a-001');
      const hash = builder.computeHash(snapshot);

      expect(hash).toHaveLength(16);
      expect(hash).toMatch(/^[0-9a-f]{16}$/);
    });
  });
});
