import { ReviewCardGenerationProjector } from './review-card-generation-projector';
import type { ProjectionContext } from './result-projector.interface';

// ═══════════════════════════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════════════════════════

function makeTx(): any {
  const store: Record<string, any[]> = {
    aiJobArtifact: [],
    reviewCard: [],
  };

  return {
    aiJobArtifact: {
      findMany: jest.fn(async (args: any) => {
        return store.aiJobArtifact.filter(
          (a) => a.jobId === args.where.jobId,
        );
      }),
      create: jest.fn(async (args: any) => {
        // Simulate P2002 if duplicate (same jobId + artifactType + artifactId)
        const dup = store.aiJobArtifact.find(
          (a) =>
            a.jobId === args.data.jobId &&
            a.artifactType === args.data.artifactType &&
            a.artifactId === args.data.artifactId,
        );
        if (dup) {
          const err: any = new Error('Unique constraint failed');
          err.code = 'P2002';
          throw err;
        }
        store.aiJobArtifact.push(args.data);
        return args.data;
      }),
    },
    reviewCard: {
      create: jest.fn(async (args: any) => {
        const card = {
          id: `rc_${store.reviewCard.length + 1}_${Math.random().toString(36).slice(2, 8)}`,
          ...args.data,
        };
        store.reviewCard.push(card);
        return card;
      }),
    },
    _store: store,
  };
}

function makeContext(overrides?: Partial<ProjectionContext>): ProjectionContext {
  return {
    job: {
      id: 'child-job-001',
      userId: 'u-001',
      jobType: 'review_card_generation',
      targetType: 'ai_analysis_result',
      targetId: 'fe_parent-001',
      snapshotId: 'snap-001',
      promptVersion: '1.0.0',
      outputSchemaVersion: 'review-card-generation-v1',
    },
    snapshot: {
      snapshot: {
        knowledgeItemId: 'ki-001',
        knowledgeBaseId: 'kb-001',
        parentJobId: 'parent-001',
      },
    },
    validatedOutput: {
      cards: [
        { frontText: 'Q1?', backText: 'A1.', difficulty: 'easy', tags: ['bio'] },
        { frontText: 'Q2?', backText: 'A2.', difficulty: 'medium', tags: [] },
      ],
      totalCount: 2,
    },
    ...overrides,
  };
}

// ═══════════════════════════════════════════════════════════════════════════
// ReviewCardGenerationProjector
// ═══════════════════════════════════════════════════════════════════════════

describe('ReviewCardGenerationProjector', () => {
  let projector: ReviewCardGenerationProjector;

  beforeEach(() => {
    projector = new ReviewCardGenerationProjector();
  });

  describe('projectorKey', () => {
    it('key 必须为 review_card_generation_projector', () => {
      expect(projector.key).toBe('review_card_generation_projector');
    });
  });

  describe('正常投影', () => {
    it('从 validatedOutput.cards 创建 ReviewCard × N + Artifact × N', async () => {
      const tx = makeTx();
      const ctx = makeContext();

      const artifacts = await projector.project(tx, ctx);

      expect(artifacts).toHaveLength(2);
      expect(artifacts[0].artifactType).toBe('ReviewCard');
      expect(artifacts[0].ordinal).toBe(0);
      expect(artifacts[1].artifactType).toBe('ReviewCard');
      expect(artifacts[1].ordinal).toBe(1);

      // 2 ReviewCards created
      expect(tx.reviewCard.create).toHaveBeenCalledTimes(2);

      // 2 Artifacts created
      expect(tx.aiJobArtifact.create).toHaveBeenCalledTimes(2);
    });

    it('ReviewCard 字段写入与 Legacy 一致', async () => {
      const tx = makeTx();
      const ctx = makeContext();

      await projector.project(tx, ctx);

      const firstCardCall = tx.reviewCard.create.mock.calls[0][0].data;
      expect(firstCardCall.userId).toBe('u-001');
      expect(firstCardCall.frontText).toBe('Q1?');
      expect(firstCardCall.backText).toBe('A1.');
      expect(firstCardCall.difficulty).toBe('easy');
      expect(firstCardCall.status).toBe('active');
      expect(firstCardCall.intervalDays).toBe(1);
      expect(firstCardCall.easeFactor).toBe(2.5);
      expect(firstCardCall.repetitionCount).toBe(0);
      expect(firstCardCall.lapseCount).toBe(0);
      expect(firstCardCall.scheduleState).toBe('new');
      expect(firstCardCall.nextReviewAt).toBeInstanceOf(Date);
    });

    it('knowledgeItemId 从 Snapshot 读取', async () => {
      const tx = makeTx();
      const ctx = makeContext({
        snapshot: {
          snapshot: { knowledgeItemId: 'ki-from-snapshot' },
        },
      });

      await projector.project(tx, ctx);

      const call = tx.reviewCard.create.mock.calls[0][0].data;
      expect(call.knowledgeItemId).toBe('ki-from-snapshot');
    });

    it('knowledgeItemId 为 null 当 Snapshot 缺失', async () => {
      const tx = makeTx();
      const ctx = makeContext({ snapshot: undefined });

      await projector.project(tx, ctx);

      const call = tx.reviewCard.create.mock.calls[0][0].data;
      expect(call.knowledgeItemId).toBeNull();
    });

    it('difficulty 默认为 medium', async () => {
      const tx = makeTx();
      const ctx = makeContext({
        validatedOutput: {
          cards: [{ frontText: 'Q', backText: 'A' }],
          totalCount: 1,
        },
      });

      await projector.project(tx, ctx);

      const call = tx.reviewCard.create.mock.calls[0][0].data;
      expect(call.difficulty).toBe('medium');
    });

    it('空 cards 数组不创建任何记录', async () => {
      const tx = makeTx();
      const ctx = makeContext({
        validatedOutput: { cards: [], totalCount: 0 },
      });

      const artifacts = await projector.project(tx, ctx);

      expect(artifacts).toHaveLength(0);
      expect(tx.reviewCard.create).not.toHaveBeenCalled();
    });
  });

  describe('入口幂等', () => {
    it('已有 Artifact → 直接返回（不重复创建）', async () => {
      const tx = makeTx();
      const ctx = makeContext();

      // First call: creates everything
      await projector.project(tx, ctx);
      expect(tx.reviewCard.create).toHaveBeenCalledTimes(2);

      // Second call: returns existing artifacts
      const artifacts = await projector.project(tx, ctx);
      expect(artifacts).toHaveLength(2);
      // No additional ReviewCard or Artifact creation
      expect(tx.reviewCard.create).toHaveBeenCalledTimes(2); // still 2
      expect(tx.aiJobArtifact.create).toHaveBeenCalledTimes(2); // still 2
    });

    it('返回的 Artifact 引用包含正确的 ordinal 排序', async () => {
      const tx = makeTx();
      const ctx = makeContext();

      // First call creates them
      await projector.project(tx, ctx);

      // Second call reads existing
      const artifacts = await projector.project(tx, ctx);
      expect(artifacts[0].ordinal).toBe(0);
      expect(artifacts[1].ordinal).toBe(1);
    });
  });

  describe('失败回滚', () => {
    it('中间卡片创建失败 → 前面的卡片回滚（事务行为）', async () => {
      const tx = makeTx();

      // First card succeeds, second fails
      let callCount = 0;
      tx.reviewCard.create = jest.fn(async () => {
        callCount++;
        if (callCount === 2) {
          throw new Error('DB insert failed');
        }
        return { id: `card-${callCount}` };
      });

      const ctx = makeContext();

      await expect(projector.project(tx, ctx)).rejects.toThrow('DB insert failed');

      // In a real transaction, the first card would be rolled back.
      // In this test, the mock doesn't support true rollback,
      // but the error propagation is verified.
    });

    it('Artifact 创建失败 → 整体回滚（事务行为）', async () => {
      const tx = makeTx();

      // Artifact creation fails on second card
      let artifactCallCount = 0;
      const origCreate = tx.aiJobArtifact.create;
      tx.aiJobArtifact.create = jest.fn(async (args: any) => {
        artifactCallCount++;
        if (artifactCallCount === 2) {
          throw new Error('Artifact insert failed');
        }
        return origCreate(args);
      });

      const ctx = makeContext();

      await expect(projector.project(tx, ctx)).rejects.toThrow('Artifact insert failed');
    });
  });

  describe('Artifact 幂等（P2002）', () => {
    it('P2002 冲突 → 静默跳过 DB 写入，仍返回 artifact 引用', async () => {
      const tx = makeTx();

      // Simulate: artifact already exists in DB (P2002 on create)
      tx.aiJobArtifact.create = jest.fn(async () => {
        const err: any = new Error('Unique constraint failed');
        err.code = 'P2002';
        throw err;
      });

      // Entry check shows no artifacts (edge case, but handled)
      tx.aiJobArtifact.findMany = jest.fn().mockResolvedValueOnce([]);

      const ctx = makeContext();

      // Should not throw — P2002 caught by upsertArtifact
      const artifacts = await projector.project(tx, ctx);
      // Artifact references still returned (they exist in DB)
      expect(artifacts).toHaveLength(2);
      // ReviewCards were still created
      expect(tx.reviewCard.create).toHaveBeenCalledTimes(2);
    });
  });

  describe('Parent 状态隔离', () => {
    it('不修改 Parent Job 的任何字段', async () => {
      const tx = makeTx();
      const ctx = makeContext();

      await projector.project(tx, ctx);

      // Verify no calls to modify parent job
      // Projector only uses tx.aiJobArtifact and tx.reviewCard
      // (verified by not having aiJob.update in mock)
      expect(tx.aiJobArtifact.findMany).toHaveBeenCalled();
      expect(tx.reviewCard.create).toHaveBeenCalled();
    });
  });

  describe('并发安全', () => {
    it('相同 jobId 并发执行：第一个成功，第二个幂等返回', async () => {
      // Simulate serial execution (Engine CAS ensures only one worker)
      const tx1 = makeTx();
      const tx2 = makeTx();

      const ctx = makeContext();

      // First projection
      const r1 = await projector.project(tx1, ctx);
      expect(r1).toHaveLength(2);

      // Second projection (e.g., retry) — uses same store so artifacts exist
      const r2 = await projector.project(tx1, ctx); // same tx = same store
      expect(r2).toHaveLength(2);

      // Artifacts only created once
      const createCalls = tx1.aiJobArtifact.create.mock.calls.length;
      // First call: 2 creates. Second call: 0 creates (entry idempotency).
      expect(createCalls).toBe(2);
    });
  });

  describe('空 cards 边缘情况', () => {
    it('cards 为 null → 不创建', async () => {
      const tx = makeTx();
      const ctx = makeContext({
        validatedOutput: { cards: null, totalCount: 0 },
      });

      const artifacts = await projector.project(tx, ctx);
      expect(artifacts).toHaveLength(0);
    });

    it('cards 为 undefined → 不创建', async () => {
      const tx = makeTx();
      const ctx = makeContext({
        validatedOutput: {},
      });

      const artifacts = await projector.project(tx, ctx);
      expect(artifacts).toHaveLength(0);
    });
  });
});
