import { Test, TestingModule } from '@nestjs/testing';
import { AiJobStateMachine, LifecycleStatus } from './ai-job-state-machine';
import { AiJobLifecycleRepository, CreateJobInput } from './ai-job-lifecycle.repository';
import { PrismaService } from '../../infrastructure/database/prisma.service';
import {
  JobLockConflictError,
  JobNotActiveError,
  JobAlreadyTerminalError,
  JobNotCancellableError,
  JobExtendLockError,
  IllegalStateTransitionError,
} from './ai-job.errors';

describe('AiJobStateMachine', () => {
  let sm: AiJobStateMachine;

  beforeEach(() => {
    sm = new AiJobStateMachine();
  });

  describe('合法迁移', () => {
    it.each([
      ['queued', 'running'],
      ['queued', 'cancelled'],
      ['running', 'succeeded'],
      ['running', 'failed'],
      ['running', 'cancelled'],
    ])('%s → %s', (from, to) => {
      expect(() => sm.validate(from as LifecycleStatus, to as LifecycleStatus)).not.toThrow();
    });
  });

  describe('非法迁移', () => {
    it.each([
      ['queued', 'succeeded'],
      ['queued', 'failed'],
      ['succeeded', 'running'],
      ['succeeded', 'failed'],
      ['succeeded', 'cancelled'],
      ['failed', 'running'],
      ['failed', 'succeeded'],
      ['failed', 'cancelled'],
      ['cancelled', 'running'],
      ['cancelled', 'succeeded'],
      ['cancelled', 'failed'],
      ['running', 'queued'],
    ])('%s → %s 抛出 IllegalStateTransitionError', (from, to) => {
      expect(() => sm.validateForJob('test-job', from as LifecycleStatus, to as LifecycleStatus))
        .toThrow(IllegalStateTransitionError);
    });
  });

  describe('终态判断', () => {
    it.each(['succeeded', 'failed', 'cancelled'] as LifecycleStatus[])('%s 是终态', (s) => {
      expect(sm.isTerminal(s)).toBe(true);
    });

    it.each(['queued', 'running'] as LifecycleStatus[])('%s 不是终态', (s) => {
      expect(sm.isTerminal(s)).toBe(false);
    });
  });

  describe('parse', () => {
    it('有效字符串返回 LifecycleStatus', () => {
      expect(sm.parse('queued')).toBe('queued');
      expect(sm.parse('running')).toBe('running');
      expect(sm.parse('succeeded')).toBe('succeeded');
    });

    it('null/undefined 返回 queued', () => {
      expect(sm.parse(null)).toBe('queued');
      expect(sm.parse(undefined)).toBe('queued');
    });

    it('无效字符串抛出错误', () => {
      expect(() => sm.parse('invalid')).toThrow(IllegalStateTransitionError);
    });
  });
});

describe('AiJobLifecycleRepository', () => {
  let repo: AiJobLifecycleRepository;
  let prisma: PrismaService;
  let tx: any; // mock transaction client

  // 伪造 Prisma TransactionClient
  const makeMockTx = () => ({
    aiJob: {
      create: jest.fn(),
      updateMany: jest.fn(),
      update: jest.fn(),
      findUnique: jest.fn(),
      findUniqueOrThrow: jest.fn(),
      findMany: jest.fn(),
    },
  });

  beforeEach(async () => {
    const txMock = makeMockTx();

    const mockPrisma = {
      aiJob: {
        create: jest.fn(),
        updateMany: jest.fn(),
        update: jest.fn(),
        findUnique: jest.fn(),
        findUniqueOrThrow: jest.fn(),
        findMany: jest.fn(),
      },
      $transaction: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AiJobStateMachine,
        AiJobLifecycleRepository,
        { provide: PrismaService, useValue: mockPrisma },
      ],
    }).compile();

    repo = module.get(AiJobLifecycleRepository);
    prisma = module.get(PrismaService);
    tx = txMock;
  });

  describe('createJob', () => {
    it('在事务中创建 queued 状态的 Job，含 Shadow Write', async () => {
      const input: CreateJobInput = {
        userId: 'user-1',
        jobType: 'test_job',
        triggerType: 'user_api',
      };
      const createdJob = {
        id: 'job-001',
        lifecycleStatus: 'queued',
        status: 'pending',
        userId: 'user-1',
        jobType: 'test_job',
        queuedAt: new Date(),
        attemptCount: 0,
        progress: 0,
      };
      tx.aiJob.create.mockResolvedValue(createdJob);

      const result = await repo.createJob(tx, input);
      expect(tx.aiJob.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            lifecycleStatus: 'queued',
            status: 'pending', // Shadow Write
            queuedAt: expect.any(Date),
          }),
        }),
      );
      expect(result.lifecycleStatus).toBe('queued');
      expect(result.status).toBe('pending');
    });
  });

  describe('lockJob', () => {
    it('CAS 抢锁成功：queued → running，含 Shadow Write', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 1 });
      (prisma.aiJob.findUniqueOrThrow as jest.Mock).mockResolvedValue({
        id: 'job-001',
        lifecycleStatus: 'running',
        status: 'processing',
        lockedBy: 'worker-1',
        lockedAt: expect.any(Date),
        lockUntil: expect.any(Date),
      });

      const result = await repo.lockJob('job-001', 'worker-1');

      const callArgs = (prisma.aiJob.updateMany as jest.Mock).mock.calls[0][0];
      expect(callArgs.where).toEqual({ id: 'job-001', lifecycleStatus: 'queued' });
      expect(callArgs.data.lifecycleStatus).toBe('running');
      expect(callArgs.data.status).toBe('processing');       // Shadow Write
      expect(callArgs.data.lockedBy).toBe('worker-1');
      expect(callArgs.data.startedAt).toBeTruthy();
      expect(callArgs.data.lockUntil).toBeTruthy();
      expect(callArgs.data.attemptCount).toEqual({ increment: 1 }); // 每次 lockJob 原子递增
      expect(result.lifecycleStatus).toBe('running');
    });

    it('CAS 抢锁失败（已进入终态）抛出 JobAlreadyTerminalError', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({
        lifecycleStatus: 'succeeded',
      });

      await expect(repo.lockJob('job-001', 'worker-1')).rejects.toThrow(JobAlreadyTerminalError);
    });

    it('CAS 抢锁失败（已被其他 Worker 锁定）抛出 JobLockConflictError', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({
        lifecycleStatus: 'running',
      });

      await expect(repo.lockJob('job-001', 'worker-1')).rejects.toThrow(JobLockConflictError);
    });
  });

  describe('extendLock', () => {
    it('续约成功返回 cancelRequested=false', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 1 });
      (prisma.aiJob.findUniqueOrThrow as jest.Mock).mockResolvedValue({
        id: 'job-001',
        lifecycleStatus: 'running',
        cancelRequestedAt: null,
      });

      const before = Date.now();
      const result = await repo.extendLock('job-001', 'worker-1');
      const after = Date.now();

      expect(result.cancelRequested).toBe(false);
      // lockUntil = now + 60000ms，允许 ±1000ms 的时钟漂移
      expect(result.lockUntil).toBeGreaterThanOrEqual(before + 60000 - 1000);
      expect(result.lockUntil).toBeLessThanOrEqual(after + 60000 + 1000);
    });

    it('续约成功返回 cancelRequested=true', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 1 });
      (prisma.aiJob.findUniqueOrThrow as jest.Mock).mockResolvedValue({
        id: 'job-001',
        lifecycleStatus: 'running',
        cancelRequestedAt: new Date(),
      });

      const result = await repo.extendLock('job-001', 'worker-1');
      expect(result.cancelRequested).toBe(true);
    });

    it('续约失败（lockedBy 不匹配）抛出 JobExtendLockError', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({
        lifecycleStatus: 'running',
        lockedBy: 'worker-2',
      });

      await expect(repo.extendLock('job-001', 'worker-1')).rejects.toThrow(JobExtendLockError);
    });

    it('续约失败（Job 不在 running 状态）抛出 JobNotActiveError', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({
        lifecycleStatus: 'succeeded', // terminal, not running
        lockedBy: null,
      });

      await expect(repo.extendLock('job-001', 'worker-1')).rejects.toThrow(JobNotActiveError);
    });
  });

  describe('markSucceeded', () => {
    it('CAS 标记成功：running → succeeded，清除错误和时间字段', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 1 });
      (prisma.aiJob.findUniqueOrThrow as jest.Mock).mockResolvedValue({
        id: 'job-001',
        lifecycleStatus: 'succeeded',
        status: 'completed',
        finishedAt: new Date(),
      });

      const result = await repo.markSucceeded('job-001');

      // 验证 CAS where 条件
      const callArgs = (prisma.aiJob.updateMany as jest.Mock).mock.calls[0][0];
      expect(callArgs.where).toEqual({ id: 'job-001', lifecycleStatus: 'running' });
      expect(callArgs.data.lifecycleStatus).toBe('succeeded');
      expect(callArgs.data.status).toBe('completed');           // Shadow Write
      expect(callArgs.data.finishedAt).toBeTruthy();
      expect(callArgs.data.errorCode).toBeNull();                // 成功时清除错误
      expect(callArgs.data.lockedBy).toBeNull();                 // 清除运行时字段
      expect(result.lifecycleStatus).toBe('succeeded');
    });

    it('CAS 失败（已终态）抛出 JobAlreadyTerminalError', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({
        lifecycleStatus: 'failed',
      });

      await expect(repo.markSucceeded('job-001')).rejects.toThrow(JobAlreadyTerminalError);
    });

    it('接受可选 tx 参数，复用调用方事务', async () => {
      tx.aiJob.updateMany.mockResolvedValue({ count: 1 });
      tx.aiJob.findUniqueOrThrow.mockResolvedValue({
        id: 'job-001',
        lifecycleStatus: 'succeeded',
      });

      await repo.markSucceeded('job-001', tx);
      expect(tx.aiJob.updateMany).toHaveBeenCalled();
      // 不应该调用 this.prisma
    });

    it('CAS 失败（非终态，如 queued）抛出 JobNotActiveError', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({
        lifecycleStatus: 'queued', // not running, not terminal
      });

      await expect(repo.markSucceeded('job-001')).rejects.toThrow(JobNotActiveError);
    });
  });

  describe('markFailed', () => {
    it('CAS 标记失败：running → failed，含错误信息', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 1 });
      (prisma.aiJob.findUniqueOrThrow as jest.Mock).mockResolvedValue({
        id: 'job-001',
        lifecycleStatus: 'failed',
        status: 'failed',
        errorCode: 'PROVIDER_TIMEOUT',
      });

      const result = await repo.markFailed('job-001', {
        errorCode: 'PROVIDER_TIMEOUT',
        publicErrorMessage: 'AI 服务暂时不可用',
        internalErrorMessage: 'DeepSeek API timeout after 30s',
      });

      expect(prisma.aiJob.updateMany).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            lifecycleStatus: 'failed',
            status: 'failed',
            errorCode: 'PROVIDER_TIMEOUT',
            publicErrorMessage: 'AI 服务暂时不可用',
            internalErrorMessage: 'DeepSeek API timeout after 30s',
          }),
        }),
      );
      expect(result.lifecycleStatus).toBe('failed');
    });

    it('CAS 失败（已终态）抛出 JobAlreadyTerminalError', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({
        lifecycleStatus: 'succeeded',
      });

      await expect(
        repo.markFailed('job-001', { errorCode: 'TEST' }),
      ).rejects.toThrow(JobAlreadyTerminalError);
    });

    it('接受可选 tx 参数，复用调用方事务', async () => {
      tx.aiJob.updateMany.mockResolvedValue({ count: 1 });
      tx.aiJob.findUniqueOrThrow.mockResolvedValue({
        id: 'job-001',
        lifecycleStatus: 'failed',
        errorCode: 'PROVIDER_TIMEOUT',
      });

      await repo.markFailed('job-001', { errorCode: 'PROVIDER_TIMEOUT' }, tx);
      expect(tx.aiJob.updateMany).toHaveBeenCalled();
    });
  });

  describe('markCancelled', () => {
    it('从 queued 取消：queued → cancelled', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 1 });
      (prisma.aiJob.findUniqueOrThrow as jest.Mock).mockResolvedValue({
        id: 'job-001',
        lifecycleStatus: 'cancelled',
        status: 'failed',
        errorCode: 'CANCELLED',
        finishedAt: new Date(),
      });

      const result = await repo.markCancelled('job-001');
      expect(prisma.aiJob.updateMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            lifecycleStatus: { in: ['queued', 'running'] },
          }),
          data: expect.objectContaining({
            lifecycleStatus: 'cancelled',
            status: 'failed', // Shadow Write
            errorCode: 'CANCELLED',
          }),
        }),
      );
      expect(result.errorCode).toBe('CANCELLED');
    });

    it('取消已终态的 Job 抛出 JobNotCancellableError', async () => {
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({
        lifecycleStatus: 'succeeded',
      });

      await expect(repo.markCancelled('job-001')).rejects.toThrow(JobNotCancellableError);
    });
  });

  describe('requestCancellation', () => {
    it('queued Job 直接取消', async () => {
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({ lifecycleStatus: 'queued' });
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 1 });
      (prisma.aiJob.findUniqueOrThrow as jest.Mock).mockResolvedValue({
        id: 'job-001',
        lifecycleStatus: 'cancelled',
        errorCode: 'CANCELLED',
      });

      const result = await repo.requestCancellation('job-001');
      expect(result.status).toBe('cancelled');
    });

    it('running Job CAS 写入 cancelRequestedAt 信号', async () => {
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({ lifecycleStatus: 'running' });
      // updateMany (CAS) succeeds
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 1 });

      const result = await repo.requestCancellation('job-001');
      expect(result.status).toBe('cancel_requested');
      expect(prisma.aiJob.updateMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: 'job-001', lifecycleStatus: 'running' },
          data: { cancelRequestedAt: expect.any(Date) },
        }),
      );
    });

    it('running Job TOCTOU：CAS 失败且已变为 cancelled → 幂等返回', async () => {
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({ lifecycleStatus: 'running' });
      // CAS fails (job already cancelled between read and write)
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      // Re-read: already cancelled
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValueOnce({ lifecycleStatus: 'running' });
      // The second findUnique call (after CAS failure)
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValueOnce({ lifecycleStatus: 'cancelled' });

      const result = await repo.requestCancellation('job-001');
      expect(result.status).toBe('cancelled'); // 幂等返回
    });

    it('running Job TOCTOU：CAS 失败且已变为 succeeded → 抛出错误', async () => {
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({ lifecycleStatus: 'running' });
      (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 });
      // Re-read: already succeeded (terminal)
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValueOnce({ lifecycleStatus: 'succeeded' });

      await expect(repo.requestCancellation('job-001')).rejects.toThrow(JobNotCancellableError);
    });

    it('终态 Job 不可取消 → 抛出 JobNotCancellableError', async () => {
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({ lifecycleStatus: 'succeeded' });

      await expect(repo.requestCancellation('job-001')).rejects.toThrow(JobNotCancellableError);
    });
  });

  describe('findByLifecycleStatus', () => {
    it('按状态过滤并按优先级/创建时间排序', async () => {
      (prisma.aiJob.findMany as jest.Mock).mockResolvedValue([]);

      await repo.findByLifecycleStatus(['queued'], { queueName: 'ai-interactive', limit: 10 });
      expect(prisma.aiJob.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            lifecycleStatus: { in: ['queued'] },
            queueName: 'ai-interactive',
          },
          orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }],
          take: 10,
        }),
      );
    });
  });

  describe('findExpiredLocks', () => {
    it('查找锁过期的 running Job', async () => {
      (prisma.aiJob.findMany as jest.Mock).mockResolvedValue([]);

      await repo.findExpiredLocks(90000);
      expect(prisma.aiJob.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            lifecycleStatus: 'running',
            lockUntil: { lt: expect.any(Date) },
          },
        }),
      );
    });
  });

  describe('findById', () => {
    it('按 ID 查找 Job', async () => {
      const mockJob = { id: 'job-001', lifecycleStatus: 'queued' };
      (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue(mockJob);

      const result = await repo.findById('job-001');
      expect(prisma.aiJob.findUnique).toHaveBeenCalledWith({ where: { id: 'job-001' } });
      expect(result).toEqual(mockJob);
    });
  });
});
