import { BadRequestException, NotFoundException } from '@nestjs/common';
import { AiReviewService } from './ai-review.service';
import type { ExcelSheetRows } from './ai-excel-reader.service';

function createService(overrides: Record<string, unknown> = {}) {
  const reviews = {
    findOne: jest.fn(),
    save: jest.fn(async (value) => value),
    create: jest.fn((value) => value),
    ...overrides,
  };
  const dataSource = {
    getRepository: jest.fn(() => ({
      find: jest.fn().mockResolvedValue([]),
      findOne: jest.fn().mockResolvedValue(null),
    })),
  };
  const service = new AiReviewService(reviews as never, dataSource as never);
  return { service, reviews, dataSource };
}

const baseArgs = {
  userId: 7,
  conversationId: 3,
  assistantMessageId: 12,
};

const validSchema = {
  title: '新生入住批量导入',
  summary: '来自报名 Excel',
  sections: [
    {
      key: 'students',
      type: 'students',
      title: '学生',
      kind: 'table',
      columns: [
        { key: 'name', title: '姓名' },
        { key: 'phone', title: '手机号' },
      ],
      rows: [
        { name: '张三', phone: '13800138000' },
        { name: '李四', phone: '13900139000' },
      ],
      issues: [],
    },
  ],
};

describe('AiReviewService', () => {
  describe('createReview', () => {
    function bigSchema(sectionCount: number) {
      const columns = Array.from({ length: 30 }, (_, i) => ({
        key: `c${i + 1}`,
        title: `列${i + 1}`,
      }));
      const cell = '中'.repeat(200);
      const rows = Array.from({ length: 500 }, (_, _i) =>
        Object.fromEntries(columns.map((column) => [column.key, cell])),
      );
      return {
        title: '大体积导入',
        summary: null,
        sections: Array.from({ length: sectionCount }, (_, i) => ({
          key: (['students', 'rooms', 'transfers'] as const)[i],
          title: `学生${i + 1}`,
          kind: 'table',
          columns,
          rows,
          issues: [],
        })),
      };
    }

    it('校验通过的 schema 落库并保留完整 sections', async () => {
      const { service, reviews } = createService();
      const review = await service.createReview(baseArgs, validSchema);
      expect(reviews.create).toHaveBeenCalledWith(
        expect.objectContaining({
          userId: 7,
          conversationId: 3,
          assistantMessageId: 12,
          title: '新生入住批量导入',
          status: 'pending',
        }),
      );
      expect(review.id).toBeTruthy();
      const sections = JSON.parse(review.sectionsJson) as unknown[];
      expect(sections).toHaveLength(1);
      expect((sections[0] as { rows: unknown[] }).rows).toHaveLength(2);
    });

    it('允许大体积合法预览（远超 256KB），不再因字节上限失败', async () => {
      const { service } = createService();
      const review = await service.createReview(baseArgs, bigSchema(1));
      expect(review.sectionsJson.length).toBeGreaterThan(256 * 1024);
    });

    it('超过 12MB 总上限的预览仍被拒绝', async () => {
      const { service } = createService();
      await expect(service.createReview(baseArgs, bigSchema(2))).rejects.toThrow('预览数据过大');
    });

    it.each([
      ['标题缺失', { sections: validSchema.sections }, '预览标题'],
      ['未知顶层字段', { ...validSchema, hack: 1 }, '未知属性'],
      ['分表为空', { ...validSchema, sections: [] }, '至少需要一个分表'],
      [
        '分表超过20个',
        {
          ...validSchema,
          sections: Array.from({ length: 21 }, (_, i) => ({
            ...validSchema.sections[0],
            key: `students_${i}`,
            title: `分表${i}`,
          })),
        },
        '不能超过 20',
      ],
      [
        '分表类型无法解析',
        {
          ...validSchema,
          sections: [{ ...validSchema.sections[0], key: 'hackers', type: undefined }],
        },
        '无法解析业务类型',
      ],
      [
        '显式非法 type 被拒绝',
        {
          ...validSchema,
          sections: [{ ...validSchema.sections[0], type: 'hackers' }],
        },
        '分表业务类型不支持',
      ],
      [
        '分表标识重复',
        {
          ...validSchema,
          sections: [validSchema.sections[0], validSchema.sections[0]],
        },
        '分表标识重复',
      ],
      [
        'kind 非 table',
        {
          ...validSchema,
          sections: [{ ...validSchema.sections[0], kind: 'chart' }],
        },
        '只能是 table',
      ],
      [
        '列缺失',
        {
          ...validSchema,
          sections: [{ ...validSchema.sections[0], columns: [] }],
        },
        '至少需要一个列',
      ],
      [
        '行数超限',
        {
          ...validSchema,
          sections: [
            {
              ...validSchema.sections[0],
              rows: Array.from({ length: 501 }, (_, i) => ({ name: `学生${i}` })),
            },
          ],
        },
        '不能超过 500',
      ],
      [
        '单元格类型非法',
        {
          ...validSchema,
          sections: [
            {
              ...validSchema.sections[0],
              rows: [{ name: '张三', phone: { hack: true } }],
            },
          ],
        },
        '类型不支持',
      ],
    ])('非法 schema 被拒绝：%s', async (_name, schema, messagePart) => {
      const { service } = createService();
      await expect(service.createReview(baseArgs, schema)).rejects.toBeInstanceOf(
        BadRequestException,
      );
      await expect(service.createReview(baseArgs, schema)).rejects.toThrow(messagePart);
    });

    it('行内未知列被剔除，不写入预览', async () => {
      const { service } = createService();
      const review = await service.createReview(baseArgs, {
        ...validSchema,
        sections: [
          {
            ...validSchema.sections[0],
            rows: [{ name: '张三', phone: '13800138000', __proto_hack: 'x', token: 'abc' }],
          },
        ],
      });
      const sections = JSON.parse(review.sectionsJson) as Array<{
        rows: Array<Record<string, unknown>>;
      }>;
      expect(sections[0].rows[0]).toEqual({ name: '张三', phone: '13800138000' });
    });

    it('同一业务类型允许多张 sheet，key 保持唯一', async () => {
      const { service } = createService();
      const review = await service.createReview(baseArgs, {
        title: '多入住 sheet',
        sections: Array.from({ length: 6 }, (_, i) => ({
          key: `checkins_${i + 1}`,
          type: 'checkins',
          title: `入住${i + 1}`,
          kind: 'table',
          sheet: `Sheet${i + 1}`,
          columns: [{ key: 'name', title: '姓名' }],
          rows: [{ name: `学生${i + 1}` }],
          issues: [],
        })),
      });
      const sections = service.parseSections(review.sectionsJson);
      expect(sections).toHaveLength(6);
      expect(sections.map((section) => section.type)).toEqual(
        Array.from({ length: 6 }, () => 'checkins'),
      );
      expect(new Set(sections.map((section) => section.key)).size).toBe(6);
      expect(sections[0].sheet).toBe('Sheet1');
    });

    it('旧格式 key 带类型前缀时自动解析 type', async () => {
      const { service } = createService();
      const review = await service.createReview(baseArgs, {
        ...validSchema,
        sections: [
          {
            key: 'checkins_girls_4',
            title: '四人间女',
            kind: 'table',
            columns: [{ key: 'name', title: '姓名' }],
            rows: [{ name: '张三' }],
            issues: [],
          },
        ],
      });
      const sections = service.parseSections(review.sectionsJson);
      expect(sections[0].type).toBe('checkins');
      expect(sections[0].key).toBe('checkins_girls_4');
    });

    it('模型常见列名别名归一化为规范键名', async () => {
      const { service } = createService();
      const review = await service.createReview(baseArgs, {
        title: '别名测试',
        sections: [
          {
            key: 'students',
            title: '学生',
            kind: 'table',
            columns: [{ key: 'org', title: '机构' }],
            rows: [{ org: '东校区' }],
            issues: [],
          },
          {
            key: 'rooms',
            title: '宿舍',
            kind: 'table',
            columns: [
              { key: 'roomNo', title: '房间号' },
              { key: 'capacity', title: '容量' },
            ],
            rows: [{ roomNo: '4-401', capacity: 4 }],
            issues: [],
          },
          {
            key: 'transfers',
            title: '换宿',
            kind: 'table',
            columns: [
              { key: 'studentNo', title: '学号' },
              { key: 'fromRoom', title: '原宿舍' },
              { key: 'toRoom', title: '目标宿舍' },
              { key: 'date', title: '换宿日期' },
            ],
            rows: [
              {
                studentNo: 'S001',
                fromRoom: '1-101',
                toRoom: '4-401',
                date: '2026-08-10',
              },
            ],
            issues: [],
          },
        ],
      });
      const sections = service.parseSections(review.sectionsJson);
      expect(sections[0].columns[0].key).toBe('organization');
      expect(sections[0].rows[0]).toEqual({ organization: '东校区' });
      expect(sections[1].columns[0].key).toBe('roomNumber');
      expect(sections[1].rows[0]).toEqual({ roomNumber: '4-401', capacity: 4 });
      expect(sections[2].rows[0]).toEqual({
        studentNo: 'S001',
        oldRoom: '1-101',
        newRoom: '4-401',
        transferDate: '2026-08-10',
      });
    });

    it('预览生成时按数据库校验机构、重复与换宿对象并追加 issues', async () => {
      const { service } = createService();
      const review = await service.createReview(baseArgs, {
        title: '预览校验',
        sections: [
          {
            key: 'students',
            title: '学生',
            kind: 'table',
            columns: [
              { key: 'name', title: '姓名' },
              { key: 'phone', title: '手机号' },
              { key: 'organization', title: '机构' },
            ],
            rows: [{ name: '小王', phone: '13800138000', organization: '不存在的机构' }],
            issues: [],
          },
          {
            key: 'rooms',
            title: '宿舍',
            kind: 'table',
            columns: [{ key: 'roomNumber', title: '房间号' }],
            rows: [{ roomNumber: '9-901' }],
            issues: [],
          },
          {
            key: 'transfers',
            title: '换宿',
            kind: 'table',
            columns: [
              { key: 'studentNo', title: '学号' },
              { key: 'newRoom', title: '目标宿舍' },
            ],
            rows: [{ studentNo: 'S001', newRoom: '9-901' }],
            issues: [],
          },
        ],
      });
      const sections = service.parseSections(review.sectionsJson);
      const studentIssues = sections.find((section) => section.key === 'students')?.issues ?? [];
      const transferIssues = sections.find((section) => section.key === 'transfers')?.issues ?? [];
      expect(studentIssues).toEqual(
        expect.arrayContaining([expect.stringContaining('所属机构无法识别')]),
      );
      expect(transferIssues).toEqual(
        expect.arrayContaining([expect.stringContaining('学生不存在')]),
      );
    });
  });

  describe('buildSectionsFromWorkbook', () => {
    const workbook: ExcelSheetRows[] = [
      {
        name: '学生名单',
        rows: [
          ['姓名', '手机号', '学号', '性别', '备注'],
          ['张三', '13800138000', 'S001', '男', ''],
          ['李四', '13900139000', 'S002', '女', '新生'],
          ['', '', '', '', ''],
        ],
      },
      {
        name: '宿舍安排',
        rows: [
          ['宿舍号', '容量', '楼栋', '楼层'],
          ['3-301', '4', '3号楼', '3'],
          ['3-302', '6', '3号楼', '3'],
        ],
      },
    ];

    it('按表头自动映射并保留文件原始行数据，未识别列进入 issues', async () => {
      const { service } = createService();
      const sections = await service.buildSectionsFromWorkbook(workbook, {
        sections: [{ key: 'students', title: '学生', sheet: '学生名单' }],
      });
      expect(sections).toHaveLength(1);
      expect(sections[0].rows).toEqual([
        { name: '张三', phone: '13800138000', studentNo: 'S001', gender: '男' },
        { name: '李四', phone: '13900139000', studentNo: 'S002', gender: '女' },
      ]);
      expect(sections[0].columns.map((column) => column.key)).toEqual([
        'name',
        'phone',
        'studentNo',
        'gender',
      ]);
      expect(sections[0].issues.join('')).toContain('备注');
    });

    it('支持显式 sourceHeader 列映射与 headerRow', async () => {
      const { service } = createService();
      const custom: ExcelSheetRows[] = [
        {
          name: 'Sheet1',
          rows: [['忽略行'], ['学生姓名', '联系方式'], ['王五', '13700137000']],
        },
      ];
      const sections = await service.buildSectionsFromWorkbook(custom, {
        sections: [
          {
            key: 'students',
            title: '学生',
            sheet: 'Sheet1',
            headerRow: 2,
            columns: [
              { key: 'name', title: '姓名', sourceHeader: '学生姓名' },
              { key: 'phone', title: '手机号', sourceHeader: '联系方式' },
            ],
          },
        ],
      });
      expect(sections[0].rows).toEqual([{ name: '王五', phone: '13700137000' }]);
    });

    it('入住记录表头自动映射为规范列名', async () => {
      const { service } = createService();
      const custom: ExcelSheetRows[] = [
        {
          name: '入住名单',
          rows: [
            ['宿舍号', '姓名', '手机号', '入住日期'],
            ['5-501', '於嘉丽', '13611112222', '2026-08-01'],
            ['5-502', '刘禹含', '13611113333', '2026/08/02'],
          ],
        },
      ];
      const sections = await service.buildSectionsFromWorkbook(custom, {
        sections: [{ key: 'checkins', title: '入住记录', sheet: '入住名单' }],
      });
      expect(sections[0].rows).toEqual([
        { name: '於嘉丽', phone: '13611112222', roomNumber: '5-501', checkInDate: '2026-08-01' },
        { name: '刘禹含', phone: '13611113333', roomNumber: '5-502', checkInDate: '2026/08/02' },
      ]);
    });

    it('英文表头映射为 camelCase 规范键（rooms/transfers/checkins）', async () => {
      const { service } = createService();
      const custom: ExcelSheetRows[] = [
        {
          name: 'Rooms',
          rows: [
            ['RoomNumber', 'RoomType'],
            ['3-301', '四人间'],
          ],
        },
        {
          name: 'Transfers',
          rows: [
            ['StudentNo', 'StudentPhone', 'OldRoom', 'NewRoom', 'TransferDate'],
            ['S001', '13800138000', '3-301', '3-302', '2026-08-05'],
          ],
        },
        {
          name: 'Checkins',
          rows: [
            ['StudentNo', 'RoomNumber', 'CheckInDate'],
            ['S001', '3-301', '2026-08-01'],
          ],
        },
      ];
      const sections = await service.buildSectionsFromWorkbook(custom, {
        sections: [
          { key: 'rooms', title: '宿舍', sheet: 'Rooms' },
          { key: 'transfers', title: '换宿', sheet: 'Transfers' },
          { key: 'checkins', title: '入住', sheet: 'Checkins' },
        ],
      });
      const byKey = Object.fromEntries(sections.map((section) => [section.key, section]));
      expect(byKey.rooms.rows).toEqual([{ roomNumber: '3-301', roomType: '四人间' }]);
      expect(byKey.transfers.rows).toEqual([
        {
          studentNo: 'S001',
          studentPhone: '13800138000',
          oldRoom: '3-301',
          newRoom: '3-302',
          transferDate: '2026-08-05',
        },
      ]);
      expect(byKey.checkins.rows).toEqual([
        { studentNo: 'S001', roomNumber: '3-301', checkInDate: '2026-08-01' },
      ]);
    });

    it('工作表不存在时抛出明确错误', async () => {
      const { service } = createService();
      await expect(
        service.buildSectionsFromWorkbook(workbook, {
          sections: [{ key: 'rooms', title: '宿舍', sheet: '不存在的表' }],
        }),
      ).rejects.toThrow('找不到工作表');
    });

    it('分表标识重复或非法时拒绝', async () => {
      const { service } = createService();
      await expect(
        service.buildSectionsFromWorkbook(workbook, {
          sections: [
            { key: 'students', title: '学生' },
            { key: 'students', title: '学生2' },
          ],
        }),
      ).rejects.toThrow('分表标识重复');
      await expect(
        service.buildSectionsFromWorkbook(workbook, {
          sections: [{ key: 'hackers', title: '入侵' }],
        }),
      ).rejects.toThrow('无法解析业务类型');
    });

    it('同一类型多张 sheet 合并生成，并保留各自 key/sheet', async () => {
      const { service } = createService();
      const multiSheet: ExcelSheetRows[] = [
        {
          name: '四人间女',
          rows: [
            ['姓名', '手机号', '宿舍号', '入住日期'],
            ['张三', '13800138000', '4-401', '2026-08-01'],
          ],
        },
        {
          name: '四人间男',
          rows: [
            ['姓名', '手机号', '宿舍号', '入住日期'],
            ['李四', '13900139000', '4-402', '2026-08-01'],
          ],
        },
      ];
      const sections = await service.buildSectionsFromWorkbook(multiSheet, {
        sections: [
          {
            key: 'checkins_girls_4',
            type: 'checkins',
            title: '四人间女',
            sheet: '四人间女',
          },
          {
            key: 'checkins_boys_4',
            type: 'checkins',
            title: '四人间男',
            sheet: '四人间男',
          },
        ],
      });
      expect(sections).toHaveLength(2);
      expect(sections.map((section) => [section.key, section.type, section.sheet])).toEqual([
        ['checkins_girls_4', 'checkins', '四人间女'],
        ['checkins_boys_4', 'checkins', '四人间男'],
      ]);
    });
  });

  describe('findOwnedPending', () => {
    it('只返回本人 pending 预览', async () => {
      const review = { id: 'review-1', userId: 7, status: 'pending' };
      const { service, reviews } = createService({ findOne: jest.fn().mockResolvedValue(review) });
      await expect(service.findOwnedPending('review-1', 7)).resolves.toBe(review);
      expect(reviews.findOne).toHaveBeenCalledWith({
        where: { id: 'review-1', userId: 7, status: 'pending' },
      });
    });

    it('已提交或不存在时抛 NotFound', async () => {
      const { service } = createService({ findOne: jest.fn().mockResolvedValue(null) });
      await expect(service.findOwnedPending('review-1', 7)).rejects.toBeInstanceOf(
        NotFoundException,
      );
    });
  });

  describe('findPendingByAssistantMessage', () => {
    it('按 assistant 消息返回最新的 pending 预览', async () => {
      const review = { id: 'review-1', assistantMessageId: 12, status: 'pending' };
      const { service, reviews } = createService({ findOne: jest.fn().mockResolvedValue(review) });
      await expect(service.findPendingByAssistantMessage(12)).resolves.toBe(review);
      expect(reviews.findOne).toHaveBeenCalledWith({
        where: { assistantMessageId: 12, status: 'pending' },
        order: { createdAt: 'DESC' },
      });
    });

    it('没有待确认预览时返回 null', async () => {
      const { service } = createService({ findOne: jest.fn().mockResolvedValue(null) });
      await expect(service.findPendingByAssistantMessage(12)).resolves.toBeNull();
    });
  });

  describe('serialize', () => {
    it('回传前端所需结构', () => {
      const { service } = createService();
      const serialized = service.serialize({
        id: 'review-1',
        title: '批量导入',
        summary: '说明',
        sectionsJson: JSON.stringify(validSchema.sections),
        status: 'submitted',
        resultSummary: '{}',
      } as never);
      expect(serialized).toEqual({
        id: 'review-1',
        title: '批量导入',
        summary: '说明',
        sections: validSchema.sections.map((section) => ({
          ...section,
          status: 'pending',
          resultSummary: null,
          submittedAt: null,
        })),
        status: 'submitted',
        resultSummary: '{}',
      });
    });
  });
});
