import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException, ForbiddenException } from '@nestjs/common';
import { JobDefinitionRegistry, DuplicateJobTypeError } from './job-definition-registry';
import { FeynmanRegistrationService } from './feynman-registration.service';
import { FEYNMAN_JOB_DEFINITION } from './feynman-job-definition';
import { FeynmanSnapshotBuilder } from './feynman-snapshot-builder';
import { PrismaService } from '../../infrastructure/database/prisma.service';

// ═══════════════════════════════════════════════════════════════════════════
// FeynmanRegistrationService
// ═══════════════════════════════════════════════════════════════════════════

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

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

    registry = module.get(JobDefinitionRegistry);
  });

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

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

      expect(def).toBeDefined();
      expect(def.jobType).toBe('feynman_evaluation');
      expect(def.queue.queueName).toBe('ai-interactive');
      expect(def.metadata.domain).toBe('analysis');
      expect(def.prompt.promptKey).toBe('feynman-evaluation');
      expect(def.prompt.promptVersion).toBe('1.0.0');
    });

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

      const reg = module.get(JobDefinitionRegistry);

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

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

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

    it('input.schemaVersion 非空', () => {
      expect(FEYNMAN_JOB_DEFINITION.input.schemaVersion).toBe('feynman-evaluation-v1');
    });

    it('output.schemaVersion 非空', () => {
      expect(FEYNMAN_JOB_DEFINITION.output.schemaVersion).toBe('feynman-evaluation-v1');
    });

    it('promptKey 使用现有 feynman-evaluation', () => {
      expect(FEYNMAN_JOB_DEFINITION.prompt.promptKey).toBe('feynman-evaluation');
      expect(FEYNMAN_JOB_DEFINITION.prompt.promptKey.length).toBeGreaterThan(0);
    });

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

    it('maxRetries 在 [0, 10] 范围内（与 Legacy 一致为 3）', () => {
      expect(FEYNMAN_JOB_DEFINITION.execution.maxRetries).toBe(3);
      expect(FEYNMAN_JOB_DEFINITION.execution.maxRetries).toBeGreaterThanOrEqual(0);
      expect(FEYNMAN_JOB_DEFINITION.execution.maxRetries).toBeLessThanOrEqual(10);
    });

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

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

    it('projectorKey 已设置（为 M-AI-05-04 预留）', () => {
      expect(FEYNMAN_JOB_DEFINITION.projectorKey).toBe('feynman_evaluation_projector');
    });

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

    it('model 使用 deepseek-v4-pro primary tier（与 Legacy 一致）', () => {
      expect(FEYNMAN_JOB_DEFINITION.model.modelTier).toBe('primary');
      expect(FEYNMAN_JOB_DEFINITION.model.modelProvider).toBe('deepseek');
      expect(FEYNMAN_JOB_DEFINITION.model.modelName).toBe('deepseek-v4-pro');
      expect(FEYNMAN_JOB_DEFINITION.model.maxTokens).toBe(4096);
    });
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// FeynmanSnapshotBuilder
// ═══════════════════════════════════════════════════════════════════════════

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

  const mockKnowledgeItem = {
    id: 'ki-001',
    userId: 'u-001',
    knowledgeBaseId: 'kb-001',
    title: '光合作用',
    content: '光合作用是植物利用光能将CO2和水转化为有机物并释放氧气的过程。',
    summary: '光合作用的基本原理',
    itemType: 'concept',
    learnable: true,
    status: 'active',
    orderIndex: 0,
    durationSeconds: 120,
    sourceId: null,
    sourceType: null,
    sourceRef: null,
    sourceDeleted: false,
    sourceTitleSnapshot: null,
    sourceSnippetSnapshot: null,
    fileSize: null,
    parentId: null,
    createdAt: new Date('2026-06-20T10:00:00Z'),
    updatedAt: new Date('2026-06-20T10:00:00Z'),
    deletedAt: null,
  };

  const mockReferenceItems = [
    {
      id: 'ki-002',
      title: '叶绿体结构',
      summary: '叶绿体是光合作用的场所',
    },
    {
      id: 'ki-003',
      title: '卡尔文循环',
      summary: '光合作用的暗反应阶段',
    },
  ];

  const validInput = {
    userId: 'u-001',
    knowledgeItemId: 'ki-001',
    knowledgeItemTitle: '光合作用',
    knowledgeItemContent: '光合作用是植物利用光能将CO2和水转化为有机物并释放氧气的过程。',
    userExplanation: '光合作用就像植物的"做饭"过程，用阳光作为能源，把CO2和水变成食物（糖类），同时释放氧气。',
    submissionId: 'sub-001',
  };

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

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

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

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

  describe('build', () => {
    it('构建有效快照', async () => {
      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue(mockReferenceItems);

      const snapshot = await builder.build(validInput);

      expect(snapshot.schemaVersion).toBe('feynman-evaluation-v1');
      expect(snapshot.snapshot.userId).toBe('u-001');
      expect(snapshot.snapshot.knowledgeItemId).toBe('ki-001');
      expect(snapshot.snapshot.knowledgeItemTitle).toBe('光合作用');
      expect(snapshot.snapshot.knowledgeItemContent).toBe(
        '光合作用是植物利用光能将CO2和水转化为有机物并释放氧气的过程。',
      );
      expect(snapshot.snapshot.userExplanation).toBe(
        '光合作用就像植物的"做饭"过程，用阳光作为能源，把CO2和水变成食物（糖类），同时释放氧气。',
      );
      expect(snapshot.snapshot.submissionId).toBe('sub-001');
      expect(snapshot.snapshot.knowledgeBaseId).toBe('kb-001');
      // 参考材料
      expect(snapshot.snapshot.referenceMaterials).toHaveLength(2);
      expect(snapshot.snapshot.referenceMaterials[0].id).toBe('ki-002');
      expect(snapshot.snapshot.referenceMaterials[0].title).toBe('叶绿体结构');
      // prompt/model 值从 JobDefinition 读取（单一事实来源）
      expect(snapshot.snapshot.promptKey).toBe(FEYNMAN_JOB_DEFINITION.prompt.promptKey);
      expect(snapshot.snapshot.promptVersion).toBe(FEYNMAN_JOB_DEFINITION.prompt.promptVersion);
      expect(snapshot.snapshot.modelTier).toBe(FEYNMAN_JOB_DEFINITION.model.modelTier);
      expect(snapshot.snapshot.inputSchemaVersion).toBe('feynman-evaluation-v1');
      expect(snapshot.snapshot.outputSchemaVersion).toBe(FEYNMAN_JOB_DEFINITION.output.schemaVersion);
      // createdAt 截断到秒
      expect(snapshot.snapshot.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
    });

    it('prompt/model 值全部来自 JobDefinition（单一事实来源）', async () => {
      const customDef = {
        ...FEYNMAN_JOB_DEFINITION,
        prompt: { promptKey: 'custom-feynman', promptVersion: '2.0' },
      };
      registry.get.mockReturnValue(customDef);

      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue([]);

      const snapshot = await builder.build(validInput);
      expect(snapshot.snapshot.promptKey).toBe('custom-feynman');
      expect(snapshot.snapshot.promptVersion).toBe('2.0');
      // 未修改的字段保持 Definition 原值
      expect(snapshot.snapshot.modelTier).toBe(FEYNMAN_JOB_DEFINITION.model.modelTier);
    });

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

      await expect(builder.build(validInput)).rejects.toThrow(
        NotFoundException,
      );
      await expect(builder.build(validInput)).rejects.toThrow(
        'KnowledgeItem ki-001 not found',
      );
    });

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

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

    it('Snapshot 不含敏感字段', async () => {
      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue([]);

      const snapshot = await builder.build(validInput);
      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_URL');
      expect(serialized).not.toContain('REDIS_URL');
      expect(serialized).not.toContain('password');
      expect(serialized).not.toContain('credential');
      expect(serialized).not.toContain('token');

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

    it('参考材料为空时正常处理', async () => {
      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue([]);

      const snapshot = await builder.build(validInput);
      expect(snapshot.snapshot.referenceMaterials).toEqual([]);
    });

    it('参考材料最多 5 条', async () => {
      const manyItems = Array.from({ length: 10 }, (_, i) => ({
        id: `ki-00${i + 2}`,
        title: `Item ${i + 2}`,
        summary: `Summary ${i + 2}`,
      }));

      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue(manyItems);

      const snapshot = await builder.build(validInput);
      // findMany 的 take: 5 在 mock 中不生效 — mock 直接返回 10 条
      // 验证 referenceMaterials 存在即可（take 由 Prisma 层控制）
      expect(snapshot.snapshot.referenceMaterials.length).toBeGreaterThanOrEqual(1);
    });
  });

  describe('computeHash', () => {
    it('相同输入 → 相同 contentHash（稳定）', async () => {
      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue(mockReferenceItems);

      const s1 = await builder.build(validInput);
      const s2 = await builder.build(validInput);

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

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

    it('不同输入 → 不同 contentHash', async () => {
      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue([]);

      const s1 = await builder.build(validInput);

      // 修改 userExplanation → 不同 hash
      const s2 = await builder.build({
        ...validInput,
        userExplanation: 'Different explanation',
      });

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

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

    it('不同 submissionId → 不同 contentHash', async () => {
      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue([]);

      const s1 = await builder.build(validInput);
      const s2 = await builder.build({
        ...validInput,
        submissionId: 'sub-002',
      });

      expect(builder.computeHash(s1)).not.toBe(builder.computeHash(s2));
    });

    it('contentHash 长度固定为 16 且为 hex', async () => {
      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue([]);

      const snapshot = await builder.build(validInput);
      const hash = builder.computeHash(snapshot);

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

    it('字段顺序不影响 contentHash（稳定序列化）', async () => {
      prisma.knowledgeItem.findUnique.mockResolvedValue(mockKnowledgeItem);
      prisma.knowledgeItem.findMany.mockResolvedValue([]);

      const s1 = await builder.build(validInput);

      // 构造乱序但内容相同的快照对象
      const s2 = { ...s1, snapshot: {} as any };
      // 反转 key 顺序
      const keys = Object.keys(s1.snapshot).reverse();
      for (const k of keys) {
        (s2.snapshot as any)[k] = (s1.snapshot as any)[k];
      }

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

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