import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import request from 'supertest';
import type { Repository } from 'typeorm';
import { AppModule } from '../app.module';
import type { DingTalkAttendanceResult } from '../integration/dingtalk.service';
import { DingTalkService } from '../integration/dingtalk.service';
import {
  AttendanceRecord,
  Organization,
  Role,
  Student,
  StudentDingMapping,
  User,
} from '../entities';
import { createStudentImportTemplateWorkbook } from '../students/student-import';

const LESSON_DATE = new Intl.DateTimeFormat('en-CA', {
  timeZone: 'Asia/Shanghai',
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
}).format(new Date());
const STUDENT_A_DING_ID = 'integration-student-a';
const STUDENT_B_DING_ID = 'integration-student-b';

const auth = (token: string) => ({ Authorization: `Bearer ${token}` });

function chinaWeekDay(date: string): number {
  const day = new Date(`${date}T00:00:00+08:00`).getDay();
  return day === 0 ? 7 : day;
}

function attendanceResult(
  userId: string,
  checkId: string,
  actualCheckTime: string,
): DingTalkAttendanceResult {
  return {
    userId,
    userName: '',
    workDate: LESSON_DATE,
    timeResult: 'Normal',
    locationResult: 'Normal',
    planCheckTime: `${LESSON_DATE}T00:00:00+08:00`,
    actualCheckTime,
    checkId,
    checkType: 'OnDuty',
    sourceType: 'ATM',
    deviceName: '集成测试考勤机',
    deviceId: 'integration-device',
  };
}

// Requires a fully configured attendance integration and is intentionally excluded from routine CI.
describe.skip('attendance workflow integration', () => {
  let app: INestApplication;
  let adminToken: string;
  let teacherToken: string;
  let mockedPunches: DingTalkAttendanceResult[];
  const originalEnv = {
    DB_TYPE: process.env.DB_TYPE,
    DB_DATABASE: process.env.DB_DATABASE,
    DB_SYNCHRONIZE: process.env.DB_SYNCHRONIZE,
    SEED_DEV: process.env.SEED_DEV,
    ADMIN_PASSWORD: process.env.ADMIN_PASSWORD,
  };

  beforeAll(async () => {
    process.env.DB_TYPE = 'sqlite';
    process.env.DB_DATABASE = ':memory:';
    process.env.DB_SYNCHRONIZE = 'true';
    process.env.SEED_DEV = 'true';
    process.env.ADMIN_PASSWORD = 'admin123';

    mockedPunches = [];
    const dingTalk = {
      fetchAttendanceResults: jest.fn(async () => mockedPunches),
    };

    const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
      .overrideProvider(DingTalkService)
      .useValue(dingTalk)
      .compile();

    app = moduleRef.createNestApplication();
    app.setGlobalPrefix('api');
    await app.init();

    const login = await request(app.getHttpServer())
      .post('/api/auth/login')
      .send({ username: 'admin', password: 'admin123' })
      .expect(201);
    adminToken = login.body.access_token;
  });

  afterAll(async () => {
    await app?.close();
    for (const [key, value] of Object.entries(originalEnv)) {
      if (value === undefined) delete process.env[key];
      else process.env[key] = value;
    }
  });

  it('imports students, builds a teacher class schedule, refreshes punches, and scopes reads', async () => {
    const roleRepo = app.get<Repository<Role>>(getRepositoryToken(Role));
    const userRepo = app.get<Repository<User>>(getRepositoryToken(User));
    const studentRepo = app.get<Repository<Student>>(getRepositoryToken(Student));
    const mappingRepo = app.get<Repository<StudentDingMapping>>(
      getRepositoryToken(StudentDingMapping),
    );
    const organizationRepo = app.get<Repository<Organization>>(getRepositoryToken(Organization));
    const attendanceRepo = app.get<Repository<AttendanceRecord>>(
      getRepositoryToken(AttendanceRecord),
    );

    const teacherRole = await roleRepo.findOneByOrFail({ code: 'teacher' });
    const teacherCreate = await request(app.getHttpServer())
      .post('/api/rbac/users')
      .set(auth(adminToken))
      .send({
        username: 'integration-teacher',
        password: 'teacher123',
        name: '集成测试任课教师',
        roleIds: [teacherRole.id],
      })
      .expect(201);
    expect(teacherCreate.body.message).toBe('用户创建成功');

    const teacher = await userRepo.findOneByOrFail({ username: 'integration-teacher' });
    const teacherId = teacher.id;
    const teacherLogin = await request(app.getHttpServer())
      .post('/api/auth/login')
      .send({ username: 'integration-teacher', password: 'teacher123' })
      .expect(201);
    teacherToken = teacherLogin.body.access_token;

    const host = await organizationRepo.findOneByOrFail({ isHost: true, status: 'active' });
    const workbook = createStudentImportTemplateWorkbook();
    const sheet = workbook.getWorksheet('学生基础+档案+录取')!;
    sheet.spliceRows(2, 1);
    sheet.addRow({
      phone: '13800000001',
      name: '集成学生甲',
      studentNo: 'IT001',
      organization: host.name,
    });
    sheet.addRow({
      phone: '13800000002',
      name: '集成学生乙',
      studentNo: 'IT002',
      organization: host.name,
    });
    const workbookBuffer = Buffer.from(await workbook.xlsx.writeBuffer());

    const importResult = await request(app.getHttpServer())
      .post('/api/students/import')
      .set(auth(adminToken))
      .attach('file', workbookBuffer, {
        filename: 'attendance-workflow-students.xlsx',
        contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      })
      .expect(201);
    expect(importResult.body).toMatchObject({ imported: 2, skipped: 0 });

    const [studentA, studentB] = await Promise.all([
      studentRepo.findOneByOrFail({ phone: '13800000001' }),
      studentRepo.findOneByOrFail({ phone: '13800000002' }),
    ]);
    await mappingRepo.save([
      mappingRepo.create({ dingUserId: STUDENT_A_DING_ID, studentId: studentA.id }),
      mappingRepo.create({ dingUserId: STUDENT_B_DING_ID, studentId: studentB.id }),
    ]);

    const classResult = await request(app.getHttpServer())
      .post('/api/classes')
      .set(auth(adminToken))
      .send({
        name: '集成考勤班',
        code: 'ATTENDANCE-INTEGRATION',
        classType: 'culture',
        status: 'active',
        startDate: LESSON_DATE,
        endDate: LESSON_DATE,
      })
      .expect(201);
    const classId = classResult.body.id;

    await request(app.getHttpServer())
      .post(`/api/classes/${classId}/students`)
      .set(auth(adminToken))
      .send({ studentIds: [studentA.id, studentB.id] })
      .expect(201)
      .expect(({ body }) => expect(body).toMatchObject({ added: 2, skipped: 0 }));

    await request(app.getHttpServer())
      .post(`/api/classes/${classId}/teachers`)
      .set(auth(adminToken))
      .send({ userId: teacherId, roleType: 'subject_teacher', subject: '语文' })
      .expect(201);

    const classroomResult = await request(app.getHttpServer())
      .post('/api/classrooms')
      .set(auth(adminToken))
      .send({ name: '集成测试教室', building: '测试楼', floor: 1, capacity: 30, roomType: '小' })
      .expect(201);

    const scheduleResult = await request(app.getHttpServer())
      .post('/api/class-schedules')
      .set(auth(adminToken))
      .send({
        classId,
        classroomId: classroomResult.body.id,
        weekDay: chinaWeekDay(LESSON_DATE),
        startTime: '00:00',
        endTime: '23:59',
        attendanceAdvanceMinutes: 0,
        startDate: LESSON_DATE,
        endDate: LESSON_DATE,
        subject: '语文',
        teacherId,
        scheduleType: 'INTERNAL',
      })
      .expect(201);
    const scheduleId = scheduleResult.body.id;

    const initialPull = await request(app.getHttpServer())
      .post(`/api/attendance-lessons/schedules/${scheduleId}/pull`)
      .set(auth(teacherToken))
      .send({ date: LESSON_DATE })
      .expect(201);
    expect(initialPull.body.records).toHaveLength(2);
    expect(initialPull.body.records.map((record: AttendanceRecord) => record.status)).toEqual([
      'pending',
      'pending',
    ]);

    const studentARecord = initialPull.body.records.find(
      (record: AttendanceRecord) => record.studentId === studentA.id,
    );
    await request(app.getHttpServer())
      .put(`/api/attendance-records/${studentARecord.id}`)
      .set(auth(teacherToken))
      .send({ status: 'absent', remark: '教师本地覆盖' })
      .expect(200)
      .expect(({ body }) => expect(body).toMatchObject({ status: 'absent', source: 'manual' }));

    mockedPunches = [
      attendanceResult(STUDENT_A_DING_ID, 'integration-check-a', `${LESSON_DATE}T01:00:00.000Z`),
      attendanceResult(STUDENT_B_DING_ID, 'integration-check-b', `${LESSON_DATE}T01:05:00.000Z`),
    ];

    const refreshed = await request(app.getHttpServer())
      .post(`/api/attendance-lessons/schedules/${scheduleId}/pull`)
      .set(auth(teacherToken))
      .send({ date: LESSON_DATE })
      .expect(201);
    expect(refreshed.body.records).toHaveLength(2);
    expect(
      refreshed.body.records.find((record: AttendanceRecord) => record.studentId === studentA.id),
    ).toMatchObject({ status: 'absent', source: 'manual', remark: '教师本地覆盖' });
    expect(
      refreshed.body.records.find((record: AttendanceRecord) => record.studentId === studentB.id),
    ).toMatchObject({ status: 'present', source: 'dingtalk', punchSource: 'ATM' });

    const teacherRecords = await request(app.getHttpServer())
      .get(
        `/api/attendance-records?classId=${classId}&dateFrom=${LESSON_DATE}&dateTo=${LESSON_DATE}`,
      )
      .set(auth(teacherToken))
      .expect(200);
    expect(teacherRecords.body.list).toHaveLength(2);
    const teacherView: Array<Pick<AttendanceRecord, 'studentId' | 'status' | 'source'>> =
      teacherRecords.body.list
        .map((record: AttendanceRecord) => ({
          studentId: record.studentId,
          status: record.status,
          source: record.source,
        }))
        .sort((left, right) => left.studentId - right.studentId);
    expect(teacherView).toEqual([
      { studentId: studentA.id, status: 'absent', source: 'manual' },
      { studentId: studentB.id, status: 'present', source: 'dingtalk' },
    ]);

    const adminRecords = await request(app.getHttpServer())
      .get(
        `/api/attendance-records?classId=${classId}&dateFrom=${LESSON_DATE}&dateTo=${LESSON_DATE}`,
      )
      .set(auth(adminToken))
      .expect(200);
    expect(adminRecords.body.list).toHaveLength(2);
    expect(
      adminRecords.body.list
        .map((record: AttendanceRecord) => ({
          studentId: record.studentId,
          status: record.status,
          source: record.source,
        }))
        .sort(
          (left: Pick<AttendanceRecord, 'studentId'>, right: Pick<AttendanceRecord, 'studentId'>) =>
            left.studentId - right.studentId,
        ),
    ).toEqual(teacherView);

    const unassignedClass = await request(app.getHttpServer())
      .post('/api/classes')
      .set(auth(adminToken))
      .send({
        name: '未分配教师班级',
        code: 'UNASSIGNED-INTEGRATION',
        classType: 'culture',
        status: 'active',
      })
      .expect(201);
    await request(app.getHttpServer())
      .get(`/api/attendance-records?classId=${unassignedClass.body.id}`)
      .set(auth(teacherToken))
      .expect(400);

    const persisted = await attendanceRepo.find({
      where: { classId },
      order: { studentId: 'ASC' },
    });
    expect(persisted).toHaveLength(2);
    expect(persisted).toEqual([
      expect.objectContaining({ studentId: studentA.id, status: 'absent', source: 'manual' }),
      expect.objectContaining({ studentId: studentB.id, status: 'present', source: 'dingtalk' }),
    ]);
  });
});
