import { Test, TestingModule } from '@nestjs/testing';
import { BadRequestException } from '@nestjs/common';
import { AiJobCreationService } from './ai-job-creation.service';
import { AiJobLifecycleRepository } from './ai-job-lifecycle.repository';
import { JobDefinitionRegistry } from './job-definition-registry';
import { PrismaService } from '../../infrastructure/database/prisma.service';
import { OutboxRepository } from '../../infrastructure/outbox/outbox.repository';
import { SnapshotBuilderService } from '../ai-runtime/snapshot-builder.service';
import { ActiveRecallSnapshotBuilder } from './active-recall-snapshot-builder';
import { UnknownJobTypeError } from './job-definition-registry';
import { JobDefinition } from './job-definition.types';

/** 最小合法 Definition 工厂 */
function validDef(overrides?: Partial<JobDefinition>): JobDefinition {
  return {
    jobType: 'test_job',
    metadata: { label: 'Test Job', description: '', domain: 'analysis', version: '1.0' },
    queue: { queueName: 'ai-interactive', defaultPriority: 0 },
    execution: { timeoutMs: 30000, maxRetries: 2, retryBackoff: { type: 'exponential', delay: 2000 }, cancellable: true, abortStrategy: 'fail' },
    input: { schemaVersion: '1.0.0' },
    output: { schemaVersion: '1.0.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 },
    ...overrides,
  };
}

describe('AiJobCreationService', () => {
  let service: AiJobCreationService;
  let prisma: any;
  let lifecycleRepo: any;
  let registry: any;
  let snapshotBuilder: any;
  let outboxRepo: any;

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

    lifecycleRepo = { createJob: jest.fn() };

    snapshotBuilder = { buildSnapshot: jest.fn() };

    outboxRepo = { createInTransaction: jest.fn() };

    prisma = {
      aiJob: {
        findUnique: jest.fn(),
      },
      aiJobSnapshot: { create: jest.fn() },
      $transaction: jest.fn((fn: any) => fn(prisma)), // mock tx = same prisma
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AiJobCreationService,
        { provide: PrismaService, useValue: prisma },
        { provide: AiJobLifecycleRepository, useValue: lifecycleRepo },
        { provide: JobDefinitionRegistry, useValue: registry },
        { provide: SnapshotBuilderService, useValue: snapshotBuilder },
        { provide: ActiveRecallSnapshotBuilder, useValue: { buildSnapshot: jest.fn(), build: jest.fn() } },
        { provide: OutboxRepository, useValue: outboxRepo },
      ],
    }).compile();

    service = module.get(AiJobCreationService);
  });

  describe('事务化创建', () => {
    it('在一个事务内创建 AiJob + Snapshot + Outbox', async () => {
      registry.get.mockReturnValue(validDef());
      snapshotBuilder.buildSnapshot.mockResolvedValue({ userId: 'u1', profile: {} });
      const mockJob = { id: 'job-001', lifecycleStatus: 'queued', jobType: 'test_job' };
      lifecycleRepo.createJob.mockResolvedValue(mockJob);

      const result = await service.createJob({
        userId: 'u1',
        jobType: 'test_job',
        triggerType: 'user_api',
        targetType: 'knowledge_base',
        targetId: 'kb-1',
      });

      // 验证事务调用
      expect(prisma.$transaction).toHaveBeenCalled();
      // Job created in tx
      expect(lifecycleRepo.createJob).toHaveBeenCalledWith(
        expect.any(Object), // the tx client
        expect.objectContaining({
          userId: 'u1',
          jobType: 'test_job',
          queueName: 'ai-interactive',
        }),
      );
      // Snapshot created in tx
      expect(prisma.aiJobSnapshot.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            jobId: 'job-001',
            jobType: 'test_job',
            contentHash: expect.any(String),
          }),
        }),
      );
      // Outbox created in tx
      expect(outboxRepo.createInTransaction).toHaveBeenCalledWith(
        expect.any(Object),
        expect.objectContaining({
          eventType: 'ai.job.enqueue',
          aggregateType: 'AiJob',
          aggregateId: 'job-001',
          dedupeKey: 'ai.job.enqueue:job-001',
          payload: { jobId: 'job-001' },
        }),
      );
      expect(result).toBe(mockJob);
    });

    it('事务中任一失败则全部回滚', async () => {
      registry.get.mockReturnValue(validDef());
      snapshotBuilder.buildSnapshot.mockResolvedValue({ userId: 'u1' });
      lifecycleRepo.createJob.mockResolvedValue({ id: 'job-001' });
      // Snapshot create fails
      prisma.aiJobSnapshot.create.mockRejectedValue(new Error('snapshot insert failed'));

      await expect(
        service.createJob({
          userId: 'u1',
          jobType: 'test_job',
          triggerType: 'user_api',
          targetType: 'knowledge_base',
          targetId: 'kb-1',
        }),
      ).rejects.toThrow('snapshot insert failed');

      // Outbox must NOT be called (tx rolled back)
      expect(outboxRepo.createInTransaction).not.toHaveBeenCalled();
    });
  });

  describe('幂等性', () => {
    it('并发重复创建相同 (userId, jobType, idempotencyKey) → P2002 → 返回已有 Job', async () => {
      registry.get.mockReturnValue(validDef());
      snapshotBuilder.buildSnapshot.mockResolvedValue({});

      // First request creates successfully
      lifecycleRepo.createJob.mockResolvedValueOnce({ id: 'job-001' });

      await service.createJob({
        userId: 'u1', jobType: 'test_job', triggerType: 'user_api',
        targetType: 'kb', targetId: 'kb-1', idempotencyKey: 'idem-001',
      });
      expect(lifecycleRepo.createJob).toHaveBeenCalledTimes(1);

      // Second request: transaction fails with P2002 (duplicate unique key)
      const p2002Error = new Error('Unique constraint failed') as any;
      p2002Error.code = 'P2002';
      prisma.$transaction.mockRejectedValueOnce(p2002Error);

      const existingJob = { id: 'job-001', lifecycleStatus: 'queued', jobType: 'test_job' };
      prisma.aiJob.findUniqueOrThrow = jest.fn().mockResolvedValue(existingJob);

      const result = await service.createJob({
        userId: 'u1', jobType: 'test_job', triggerType: 'user_api',
        targetType: 'kb', targetId: 'kb-1', idempotencyKey: 'idem-001',
      });

      expect(result).toBe(existingJob);
      // Snapshot was still built (pre-transaction), but no new Job created
    });

    it('未提供 idempotencyKey 时每次创建新 Job（无幂等保护）', async () => {
      registry.get.mockReturnValue(validDef());
      snapshotBuilder.buildSnapshot.mockResolvedValue({});
      const mockJob = { id: 'job-002' };
      lifecycleRepo.createJob.mockResolvedValue(mockJob);

      await service.createJob({ userId: 'u1', jobType: 'test_job', triggerType: 'user_api', targetType: 'kb', targetId: 'kb-1' });
      await service.createJob({ userId: 'u1', jobType: 'test_job', triggerType: 'user_api', targetType: 'kb', targetId: 'kb-1' });

      expect(lifecycleRepo.createJob).toHaveBeenCalledTimes(2);
    });
  });

  describe('Job 元数据从 Definition 写入', () => {
    it('queueName / priority / timeout / maxAttempts / schema 等来自 Definition', async () => {
      registry.get.mockReturnValue(
        validDef({
          queue: { queueName: 'ai-background', defaultPriority: 5 },
          execution: { timeoutMs: 90000, maxRetries: 1, retryBackoff: { type: 'exponential', delay: 5000 }, cancellable: false, abortStrategy: 'retry' },
          input: { schemaVersion: '2.0.0' },
          output: { schemaVersion: '2.0.0' },
          prompt: { promptKey: 'bg_prompt', promptVersion: '2.0' },
          model: { modelTier: 'fallback', modelProvider: 'deepseek', modelName: 'deepseek-chat' },
        }),
      );
      snapshotBuilder.buildSnapshot.mockResolvedValue({});
      const mockJob = { id: 'bg-job' };
      lifecycleRepo.createJob.mockResolvedValue(mockJob);

      await service.createJob({
        userId: 'u1',
        jobType: 'bg_job',
        triggerType: 'system_scheduled',
        targetType: 'user',
        targetId: 'u1',
      });

      expect(lifecycleRepo.createJob).toHaveBeenCalledWith(
        expect.any(Object),
        expect.objectContaining({
          queueName: 'ai-background',
          priority: 5,
          timeoutMs: 90000,
          maxAttempts: 1,
          inputSchemaVersion: '2.0.0',
          outputSchemaVersion: '2.0.0',
          promptVersion: '2.0',
          modelProvider: 'deepseek',
          modelName: 'deepseek-chat',
        }),
      );
    });

    it('overrides 覆盖 Definition 默认值', async () => {
      registry.get.mockReturnValue(validDef());
      snapshotBuilder.buildSnapshot.mockResolvedValue({});
      const mockJob = { id: 'override-job' };
      lifecycleRepo.createJob.mockResolvedValue(mockJob);

      await service.createJob({
        userId: 'u1',
        jobType: 'test_job',
        triggerType: 'user_api',
        targetType: 'kb',
        targetId: 'kb-1',
        overrides: {
          priority: 99,
          credentialMode: 'user_deepseek_key',
          credentialId: 'cred-xyz',
        },
      });

      expect(lifecycleRepo.createJob).toHaveBeenCalledWith(
        expect.any(Object),
        expect.objectContaining({
          priority: 99,
          credentialMode: 'user_deepseek_key',
          credentialId: 'cred-xyz',
        }),
      );
    });
  });

  describe('输入校验', () => {
    it('未知 jobType → UnknownJobTypeError', async () => {
      registry.get.mockImplementation(() => {
        throw new UnknownJobTypeError('bad_job');
      });

      await expect(
        service.createJob({
          userId: 'u1',
          jobType: 'bad_job',
          triggerType: 'user_api',
          targetType: 'kb',
          targetId: 'kb-1',
        }),
      ).rejects.toThrow(UnknownJobTypeError);
    });

    it('非法 triggerType → BadRequestException', async () => {
      registry.get.mockReturnValue(validDef());

      await expect(
        service.createJob({
          userId: 'u1',
          jobType: 'test_job',
          triggerType: 'evil_hack' as any,
          targetType: 'kb',
          targetId: 'kb-1',
        }),
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe('Outbox payload 约束', () => {
    it('Outbox payload 仅含 {jobId}', async () => {
      registry.get.mockReturnValue(validDef());
      snapshotBuilder.buildSnapshot.mockResolvedValue({ userId: 'u1' });
      lifecycleRepo.createJob.mockResolvedValue({ id: 'job-minimal' });

      await service.createJob({
        userId: 'u1',
        jobType: 'test_job',
        triggerType: 'user_api',
        targetType: 'kb',
        targetId: 'kb-1',
      });

      expect(outboxRepo.createInTransaction).toHaveBeenCalledWith(
        expect.any(Object),
        expect.objectContaining({
          payload: { jobId: 'job-minimal' },
        }),
      );

      // payload 不应包含 userId、credentialId 等敏感字段
      const call = outboxRepo.createInTransaction.mock.calls[0][1];
      expect(call.payload).not.toHaveProperty('userId');
      expect(call.payload).not.toHaveProperty('credentialId');
      expect(call.payload).not.toHaveProperty('snapshot');
    });
  });

  describe('Snapshot', () => {
    it('Snapshot 传入 buildSnapshot 产出的 content 和 contentHash', async () => {
      const snapContent = { userId: 'u1', profile: { level: 5 }, settings: {} };
      registry.get.mockReturnValue(validDef());
      snapshotBuilder.buildSnapshot.mockResolvedValue(snapContent);
      lifecycleRepo.createJob.mockResolvedValue({ id: 'snap-job' });

      await service.createJob({
        userId: 'u1',
        jobType: 'test_job',
        triggerType: 'user_api',
        targetType: 'kb',
        targetId: 'kb-1',
      });

      expect(prisma.aiJobSnapshot.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            jobId: 'snap-job',
            content: snapContent,
            contentHash: expect.any(String),
            schemaVersion: '1.0.0',
            redactionVersion: '1.0',
          }),
        }),
      );
    });
  });

  describe('createInTransaction', () => {
    beforeEach(() => {
      // tx mock: in test, tx === same prisma object (simplified interactive tx)
      prisma.aiJob.findUnique = jest.fn();
      prisma.aiJobSnapshot.create = jest.fn();
    });

    it('在外部事务中创建 Child Job + Snapshot + Outbox', async () => {
      registry.get.mockReturnValue(validDef({
        jobType: 'review_card_generation',
        queue: { queueName: 'ai-background', defaultPriority: 0 },
        prompt: { promptKey: 'review-card-generation', promptVersion: '1.0.0' },
      }));
      const mockChildJob = {
        id: 'child-job-001',
        lifecycleStatus: 'queued',
        jobType: 'review_card_generation',
      };
      lifecycleRepo.createJob.mockResolvedValue(mockChildJob);

      // No existing job (idempotency check passes)
      prisma.aiJob.findUnique.mockResolvedValue(null);

      const snapshotContent = {
        userId: 'u1',
        parentJobId: 'parent-job-001',
        strengths: ['a'],
        weaknesses: ['b', 'c'],
        cardCount: 2,
      };

      const result = await service.createInTransaction(
        prisma as any,
        {
          userId: 'u1',
          jobType: 'review_card_generation',
          triggerType: 'parent_job',
          targetType: 'ai_analysis_result',
          targetId: 'fe_parent-job-001',
          parentJobId: 'parent-job-001',
          idempotencyKey: 'review-card:feynman:parent-job-001',
          retrySnapshotContent: snapshotContent,
        },
      );

      // Returns the created job
      expect(result).toBe(mockChildJob);

      // Did NOT open its own transaction
      expect(prisma.$transaction).not.toHaveBeenCalled();

      // Created job via lifecycleRepo with passed tx
      expect(lifecycleRepo.createJob).toHaveBeenCalledWith(
        prisma,
        expect.objectContaining({
          userId: 'u1',
          jobType: 'review_card_generation',
          parentJobId: 'parent-job-001',
          idempotencyKey: 'review-card:feynman:parent-job-001',
          triggerType: 'parent_job',
          queueName: 'ai-background',
        }),
      );

      // Created snapshot with passed tx
      expect(prisma.aiJobSnapshot.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            jobId: 'child-job-001',
            jobType: 'review_card_generation',
            content: snapshotContent,
          }),
        }),
      );

      // Created outbox with passed tx
      expect(outboxRepo.createInTransaction).toHaveBeenCalledWith(
        prisma,
        expect.objectContaining({
          eventType: 'ai.job.enqueue',
          aggregateType: 'AiJob',
          aggregateId: 'child-job-001',
          dedupeKey: 'ai.job.enqueue:child-job-001',
          payload: { jobId: 'child-job-001' },
        }),
      );
    });

    it('rejects if retrySnapshotContent is missing', async () => {
      registry.get.mockReturnValue(validDef());

      await expect(
        service.createInTransaction(prisma as any, {
          userId: 'u1',
          jobType: 'test_job',
          triggerType: 'user_api',
          targetType: 'kb',
          targetId: 'kb-1',
        } as any),
      ).rejects.toThrow(BadRequestException);
    });

    it('幂等：相同 idempotencyKey 返回已有 Job（check-before-create）', async () => {
      registry.get.mockReturnValue(validDef({
        jobType: 'review_card_generation',
        queue: { queueName: 'ai-background', defaultPriority: 0 },
      }));

      // Existing job found via check-before-create
      const existingJob = {
        id: 'existing-child-001',
        lifecycleStatus: 'queued',
        jobType: 'review_card_generation',
      };
      prisma.aiJob.findUnique.mockResolvedValue(existingJob);

      const result = await service.createInTransaction(prisma as any, {
        userId: 'u1',
        jobType: 'review_card_generation',
        triggerType: 'parent_job',
        targetType: 'ai_analysis_result',
        targetId: 'fe_parent-001',
        parentJobId: 'parent-001',
        idempotencyKey: 'review-card:feynman:parent-001',
        retrySnapshotContent: { cardCount: 1 },
      });

      // Returns existing job without creating a new one
      expect(result).toBe(existingJob);
      expect(lifecycleRepo.createJob).not.toHaveBeenCalled();
      expect(prisma.aiJobSnapshot.create).not.toHaveBeenCalled();
      expect(outboxRepo.createInTransaction).not.toHaveBeenCalled();
    });

    it('未提供 idempotencyKey 时跳过幂等检查，正常创建', async () => {
      registry.get.mockReturnValue(validDef());
      const mockJob = { id: 'no-idem-job', lifecycleStatus: 'queued' };
      lifecycleRepo.createJob.mockResolvedValue(mockJob);

      const result = await service.createInTransaction(prisma as any, {
        userId: 'u1',
        jobType: 'test_job',
        triggerType: 'user_api',
        targetType: 'kb',
        targetId: 'kb-1',
        retrySnapshotContent: { key: 'val' },
      });

      expect(result).toBe(mockJob);
      // findUnique NOT called for idempotency check
      expect(prisma.aiJob.findUnique).not.toHaveBeenCalled();
      expect(lifecycleRepo.createJob).toHaveBeenCalledTimes(1);
    });

    it('parentJobId 正确传递给 lifecycleRepo', async () => {
      registry.get.mockReturnValue(validDef());
      lifecycleRepo.createJob.mockResolvedValue({ id: 'child-002' });

      await service.createInTransaction(prisma as any, {
        userId: 'u1',
        jobType: 'test_job',
        triggerType: 'parent_job',
        targetType: 'ai_analysis_result',
        targetId: 'fe_parent-xyz',
        parentJobId: 'parent-xyz',
        idempotencyKey: 'child-idem-002',
        retrySnapshotContent: {},
      });

      expect(lifecycleRepo.createJob).toHaveBeenCalledWith(
        prisma,
        expect.objectContaining({
          parentJobId: 'parent-xyz',
          idempotencyKey: 'child-idem-002',
          triggerType: 'parent_job',
        }),
      );
    });

    it('Outbox payload 仅含 {jobId}（不泄露敏感信息）', async () => {
      registry.get.mockReturnValue(validDef());
      lifecycleRepo.createJob.mockResolvedValue({ id: 'secure-child' });

      await service.createInTransaction(prisma as any, {
        userId: 'u1',
        jobType: 'test_job',
        triggerType: 'parent_job',
        targetType: 'kb',
        targetId: 'kb-1',
        parentJobId: 'parent-secure',
        retrySnapshotContent: { userId: 'u1', secret: 'SHOULD_NOT_LEAK' },
      });

      const call = outboxRepo.createInTransaction.mock.calls[0][1];
      expect(call.payload).toEqual({ jobId: 'secure-child' });
      expect(call.payload).not.toHaveProperty('userId');
      expect(call.payload).not.toHaveProperty('secret');
    });

    it('Snapshot 创建失败 → 错误在外部事务中传播', async () => {
      registry.get.mockReturnValue(validDef());
      lifecycleRepo.createJob.mockResolvedValue({ id: 'will-fail' });
      prisma.aiJobSnapshot.create.mockRejectedValue(
        new Error('snapshot insert failed'),
      );

      await expect(
        service.createInTransaction(prisma as any, {
          userId: 'u1',
          jobType: 'test_job',
          triggerType: 'user_api',
          targetType: 'kb',
          targetId: 'kb-1',
          retrySnapshotContent: {},
        }),
      ).rejects.toThrow('snapshot insert failed');

      // Outbox NOT created after failure
      expect(outboxRepo.createInTransaction).not.toHaveBeenCalled();
    });

    it('Outbox 创建失败 → 错误在外部事务中传播', async () => {
      registry.get.mockReturnValue(validDef());
      lifecycleRepo.createJob.mockResolvedValue({ id: 'outbox-fail' });
      outboxRepo.createInTransaction.mockRejectedValue(
        new Error('outbox insert failed'),
      );

      await expect(
        service.createInTransaction(prisma as any, {
          userId: 'u1',
          jobType: 'test_job',
          triggerType: 'user_api',
          targetType: 'kb',
          targetId: 'kb-1',
          retrySnapshotContent: {},
        }),
      ).rejects.toThrow('outbox insert failed');
    });

    it('不影响现有 createJob 行为（回归）', async () => {
      registry.get.mockReturnValue(validDef());
      snapshotBuilder.buildSnapshot.mockResolvedValue({ userId: 'u1' });
      const mockJob = { id: 'regression-job', lifecycleStatus: 'queued' };
      lifecycleRepo.createJob.mockResolvedValue(mockJob);

      const result = await service.createJob({
        userId: 'u1',
        jobType: 'test_job',
        triggerType: 'user_api',
        targetType: 'kb',
        targetId: 'kb-1',
        idempotencyKey: 'regression-key',
      });

      expect(result).toBe(mockJob);
      // createJob still uses its own transaction
      expect(prisma.$transaction).toHaveBeenCalled();
      expect(lifecycleRepo.createJob).toHaveBeenCalledWith(
        expect.any(Object),
        expect.objectContaining({ idempotencyKey: 'regression-key' }),
      );
    });
  });
});
