import { Test, TestingModule } from '@nestjs/testing';
import { JobDefinitionRegistry, DuplicateJobTypeError } from './job-definition-registry';
import { ReviewCardGenerationRegistrationService } from './review-card-generation-registration.service';
import { REVIEW_CARD_GENERATION_JOB_DEFINITION } from './review-card-generation-job-definition';
import { ReviewCardGenerationSnapshotBuilder } from './review-card-generation-snapshot-builder';

// ═══════════════════════════════════════════════════════════════════════════
// ReviewCardGenerationRegistrationService
// ═══════════════════════════════════════════════════════════════════════════

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

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

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

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

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

      expect(def).toBeDefined();
      expect(def.jobType).toBe('review_card_generation');
      expect(def.queue.queueName).toBe('ai-background');
      expect(def.metadata.domain).toBe('generation');
      expect(def.prompt.promptKey).toBe('review-card-generation');
      expect(def.prompt.promptVersion).toBe('1.0.0');
    });

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

      const reg = module.get(JobDefinitionRegistry);

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

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

    it('queueName 为 ai-background', () => {
      expect(REVIEW_CARD_GENERATION_JOB_DEFINITION.queue.queueName).toBe('ai-background');
    });

    it('input.schemaVersion 非空', () => {
      expect(REVIEW_CARD_GENERATION_JOB_DEFINITION.input.schemaVersion).toBe('review-card-generation-v1');
    });

    it('output.schemaVersion 非空', () => {
      expect(REVIEW_CARD_GENERATION_JOB_DEFINITION.output.schemaVersion).toBe('review-card-generation-v1');
    });

    it('promptKey 为 review-card-generation', () => {
      expect(REVIEW_CARD_GENERATION_JOB_DEFINITION.prompt.promptKey).toBe('review-card-generation');
      expect(REVIEW_CARD_GENERATION_JOB_DEFINITION.prompt.promptVersion).toBe('1.0.0');
    });

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

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

    it('modelTier 为 cheap', () => {
      expect(REVIEW_CARD_GENERATION_JOB_DEFINITION.model.modelTier).toBe('cheap');
    });

    it('modelName 为 deepseek-v4-flash（与 ModelRouter cheap tier 一致）', () => {
      expect(REVIEW_CARD_GENERATION_JOB_DEFINITION.model.modelName).toBe('deepseek-v4-flash');
    });

    it('maxTokens 为 2048', () => {
      expect(REVIEW_CARD_GENERATION_JOB_DEFINITION.model.maxTokens).toBe(2048);
    });

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

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

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

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

// ═══════════════════════════════════════════════════════════════════════════
// ReviewCardGenerationSnapshotBuilder
// ═══════════════════════════════════════════════════════════════════════════

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

  const validInput = {
    userId: 'u-001',
    parentJobId: 'parent-job-001',
    sourceResultId: 'fe_parent-job-001',
    knowledgeItemId: 'ki-001',
    knowledgeBaseId: 'kb-001',
    summary: '用户对光合作用有基本理解，但在光反应和暗反应的区别上存在混淆。',
    strengths: ['基本概念掌握', '术语使用正确'],
    weaknesses: ['光反应细节不准确', '暗反应遗漏关键步骤'],
  };

  beforeEach(async () => {
    registry = {
      get: jest.fn().mockReturnValue(REVIEW_CARD_GENERATION_JOB_DEFINITION),
    };

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

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

  describe('build', () => {
    it('构建有效快照', () => {
      const snapshot = builder.build(validInput);

      expect(snapshot.schemaVersion).toBe('review-card-generation-v1');
      expect(snapshot.snapshot.userId).toBe('u-001');
      expect(snapshot.snapshot.parentJobId).toBe('parent-job-001');
      expect(snapshot.snapshot.sourceResultId).toBe('fe_parent-job-001');
      expect(snapshot.snapshot.knowledgeItemId).toBe('ki-001');
      expect(snapshot.snapshot.knowledgeBaseId).toBe('kb-001');
      expect(snapshot.snapshot.title).toBe('用户对光合作用有基本理解，但在光反应和暗反应的区别上存在混淆。');
      expect(snapshot.snapshot.summary).toContain('摘要：');
      expect(snapshot.snapshot.summary).toContain('掌握点：');
      expect(snapshot.snapshot.summary).toContain('薄弱点：');
      expect(snapshot.snapshot.strengths).toEqual(['基本概念掌握', '术语使用正确']);
      expect(snapshot.snapshot.weaknesses).toEqual(['光反应细节不准确', '暗反应遗漏关键步骤']);
      expect(snapshot.snapshot.cardCount).toBe(2); // Math.min(3, Math.max(1, 2)) = 2
    });

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

      const snapshot = builder.build(validInput);
      expect(snapshot.snapshot.promptKey).toBe('custom-rc-key');
      expect(snapshot.snapshot.promptVersion).toBe('2.0');
      expect(snapshot.snapshot.modelTier).toBe('primary');
      // 未修改的字段保持 Definition 原值
      expect(snapshot.snapshot.inputSchemaVersion).toBe('review-card-generation-v1');
    });

    it('cardCount = 1 when no weaknesses', () => {
      const snapshot = builder.build({
        ...validInput,
        weaknesses: [],
      });
      expect(snapshot.snapshot.cardCount).toBe(1);
    });

    it('cardCount = 1 when weaknesses undefined', () => {
      const snapshot = builder.build({
        ...validInput,
        weaknesses: undefined as any,
      });
      expect(snapshot.snapshot.cardCount).toBe(1);
    });

    it('cardCount = 1 when single weakness', () => {
      const snapshot = builder.build({
        ...validInput,
        weaknesses: ['只有一个弱点'],
      });
      expect(snapshot.snapshot.cardCount).toBe(1);
    });

    it('cardCount = 2 when two weaknesses', () => {
      const snapshot = builder.build({
        ...validInput,
        weaknesses: ['弱点1', '弱点2'],
      });
      expect(snapshot.snapshot.cardCount).toBe(2);
    });

    it('cardCount capped at 3 when many weaknesses', () => {
      const snapshot = builder.build({
        ...validInput,
        weaknesses: ['w1', 'w2', 'w3', 'w4', 'w5'],
      });
      expect(snapshot.snapshot.cardCount).toBe(3);
    });

    it('title truncated to 80 characters', () => {
      const longSummary = 'A'.repeat(200);
      const snapshot = builder.build({
        ...validInput,
        summary: longSummary,
      });
      expect(snapshot.snapshot.title.length).toBeLessThanOrEqual(80);
      expect(snapshot.snapshot.title).toBe(longSummary.slice(0, 80));
    });

    it('title fallback when summary is empty', () => {
      const snapshot = builder.build({
        ...validInput,
        summary: '',
      });
      expect(snapshot.snapshot.title).toBe('AI 分析结果');
    });

    it('Snapshot 不含敏感字段', () => {
      const snapshot = 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');
      expect(serialized).not.toContain('connectionInfo');
      expect(serialized).not.toContain('password');
      expect(serialized).not.toContain('credential');

      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');
      expect(snap).not.toHaveProperty('providerKey');
    });

    it('Snapshot 不包含 Parent Job 全量实体', () => {
      const snapshot = builder.build(validInput);
      const snap = snapshot.snapshot as Record<string, unknown>;

      // Only contains parentJobId reference, not the full entity
      expect(snap).toHaveProperty('parentJobId');
      expect(snap).not.toHaveProperty('parentJob');
      expect(snap).not.toHaveProperty('parent');
    });

    it('createdAt 归一化到秒（截断毫秒）', () => {
      const snapshot = builder.build(validInput);
      // ISO8601 format without milliseconds: 2026-06-21T12:00:00Z
      expect(snapshot.snapshot.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
      expect(snapshot.snapshot.createdAt).not.toContain('.');
    });
  });

  describe('computeHash', () => {
    it('相同输入 → 相同 contentHash（稳定）', () => {
      const s1 = builder.build(validInput);
      const s2 = builder.build(validInput);

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

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

    it('不同输入 → 不同 contentHash', () => {
      const s1 = builder.build(validInput);
      const s2 = builder.build({
        ...validInput,
        weaknesses: ['完全不同的弱点'],
      });

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

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

    it('contentHash 长度固定为 16', () => {
      const snapshot = builder.build(validInput);
      const hash = builder.computeHash(snapshot);

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

    it('contentHash 不含随机值（多次调用相同输入→相同hash）', () => {
      const hashes = Array.from({ length: 10 }, () => {
        const s = builder.build(validInput);
        return builder.computeHash(s);
      });

      // All hashes must be identical
      const first = hashes[0];
      for (const h of hashes) {
        expect(h).toBe(first);
      }
    });
  });
});
