/**
 * Site-attendance · Checkpoint Controller L1 集成测试（缺口端点）
 *
 * 覆盖 checkpoint.controller 中 5 个尚未被 export-events.api.test.ts 覆盖的端点：
 *   GET    /site-attendance/checkpoints             (list, admin only)
 *   GET    /site-attendance/checkpoints/:id         (detail, admin only)
 *   PATCH  /site-attendance/checkpoints/:id         (update, admin only)
 *   DELETE /site-attendance/checkpoints/:id         (delete, admin only)
 *   GET    /site-attendance/checkpoints/:id/events  (events list, paginated)
 *
 * 不重复测以下 export-events.api.test.ts 已覆盖的端点：
 *   GET /:id/events/export
 *
 * 不重复测以下 checkin-duration.api.test.ts 已覆盖的端点：
 *   GET /:id/today-summary
 *
 * GET /:id/events 会批量逆向地理编码 → jest.spyOn mock GeocodingService.reverseGeocode。
 *
 * 关联工单 #341
 */

import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { PrismaService } from '@/core/database/prisma/prisma.service';
import { GeocodingService } from '@/modules/site-attendance/services/geocoding.service';
import { createTestApp } from '../../helpers/app.helper';
import { cleanupByPrefix } from '../../helpers/cleanup.helper';
import {
  createAdminUser,
  createTestUser,
} from '../../helpers/factories/user.factory';

describe('Site-attendance · Checkpoint Controller (缺口端点) - L1', () => {
  let app: INestApplication;
  let prisma: PrismaService;
  let adminToken: string;
  let userToken: string;
  let userId: string;
  let geocodingService: GeocodingService;

  // ============================================================
  // helpers
  // ============================================================

  function suffix() {
    return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  }

  async function login(username: string, password: string): Promise<string> {
    const resp = await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({ username, password })
      .expect(200);
    return resp.body.data.accessToken as string;
  }

  async function setupUsers() {
    const s = suffix();
    const admin = await createAdminUser({
      username: `t_cp_adm_${s}`,
      email: `t_cp_adm_${s}@example.com`,
      password: 'Admin@123',
      displayName: `t_CP Admin ${s}`,
    });
    const user = await createTestUser({
      username: `t_cp_usr_${s}`,
      email: `t_cp_usr_${s}@example.com`,
      password: 'User@1234',
      displayName: `t_CP User ${s}`,
    });
    userId = user.id;
    adminToken = await login(admin.username, 'Admin@123');
    userToken = await login(user.username, 'User@1234');
  }

  /** 通过 admin API 创建 checkpoint，返回 `body.data` */
  async function createCheckpoint(overrides: Record<string, any> = {}) {
    const s = suffix();
    const res = await request(app.getHttpServer())
      .post('/api/v1/site-attendance/checkpoints')
      .set('Authorization', `Bearer ${adminToken}`)
      .send({
        name: `t_CP_${s}`,
        timezone: 'Asia/Shanghai',
        latitude: 39.9042,
        longitude: 116.4074,
        accessMode: 'PUBLIC',
        geoPolicy: 'SKIP',
        ...overrides,
      })
      .expect(201);
    return res.body.data;
  }

  /** 直接注入一条签到事件（绕过 30s 防重保护）*/
  async function injectEvent(
    checkpointId: string,
    eventUserId: string,
    eventType: 'CHECK_IN' | 'CHECK_OUT',
    localDate: string,
    timestampOffset = 0,
  ) {
    return prisma.siteAttendanceEvent.create({
      data: {
        checkpointId,
        userId: eventUserId,
        eventType,
        timestamp: new Date(Date.now() - timestampOffset),
        localDate,
        authMethod: 'AUTHENTICATED',
        geoStatus: 'SKIPPED',
      },
    });
  }

  // ============================================================
  // Lifecycle
  // ============================================================

  beforeAll(async () => {
    process.env.NODE_ENV = 'test';
    app = await createTestApp();
    prisma = app.get<PrismaService>(PrismaService);
    geocodingService = app.get<GeocodingService>(GeocodingService);

    await setupUsers();
  });

  beforeEach(async () => {
    jest.restoreAllMocks();
    // 默认 mock reverseGeocode，避免每个 case 都真调外部 API
    jest.spyOn(geocodingService, 'reverseGeocode').mockResolvedValue({
      displayName: 'Mock Address',
    });
  });

  afterEach(async () => {
    await prisma.siteAttendanceEvent.deleteMany({}).catch((e: unknown) =>
      console.warn('afterEach event cleanup warn:', e),
    );
    await prisma.siteDailySummary.deleteMany({}).catch((e: unknown) =>
      console.warn('afterEach summary cleanup warn:', e),
    );
    await prisma.siteCheckpoint.deleteMany({
      where: { name: { startsWith: 't_' } },
    }).catch((e: unknown) =>
      console.warn('afterEach checkpoint cleanup warn:', e),
    );
  });

  afterAll(async () => {
    // 跨 suite orphan 保险清理
    await prisma.siteAttendanceEvent.deleteMany({}).catch(() => undefined);
    await prisma.siteDailySummary.deleteMany({}).catch(() => undefined);
    await prisma.siteCheckpoint.deleteMany({
      where: { name: { startsWith: 't_' } },
    }).catch(() => undefined);
    await cleanupByPrefix(prisma);
    await app.close();
  });

  // ============================================================
  // GET /site-attendance/checkpoints  (list)
  // ============================================================

  describe('GET /checkpoints', () => {
    it('[1] happy path: admin → 200 + checkpoints 数组', async () => {
      await createCheckpoint();
      await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.data).toHaveProperty('checkpoints');
      expect(Array.isArray(resp.body.data.checkpoints)).toBe(true);
      expect(resp.body.data.checkpoints.length).toBeGreaterThanOrEqual(2);
    });

    it('[2] 无 token → 401', async () => {
      await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints')
        .expect(401);
    });

    it('[3] 普通员工 token（无 checkpoint:manage 权限）→ 403', async () => {
      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints')
        .set('Authorization', `Bearer ${userToken}`);

      expect(resp.status).toBe(403);
    });

    it('[4] 返回的 checkpoint 对象包含必要字段', async () => {
      const cp = await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      const found = resp.body.data.checkpoints.find(
        (c: any) => c.id === cp.id,
      );
      expect(found).toBeDefined();
      expect(found).toMatchObject({
        id: cp.id,
        name: cp.name,
        timezone: 'Asia/Shanghai',
        isActive: true,
      });
    });

    it('[5] 空库时返回 { checkpoints: [] }', async () => {
      // 强清所有 checkpoint（防跨 suite 残留），断 list 为严格空列表。
      // 这是 list endpoint 最基本的契约：empty DB → empty list（不只是"是数组"）。
      // 防的是：未来 DataScope/权限 filter 写错 → 用户看到不该看的数据，case 漏 catch。
      await prisma.siteCheckpoint.deleteMany({});

      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(Array.isArray(resp.body.data.checkpoints)).toBe(true);
      expect(resp.body.data.checkpoints).toHaveLength(0);
    });
  });

  // ============================================================
  // GET /site-attendance/checkpoints/:id  (detail)
  // ============================================================

  describe('GET /checkpoints/:id', () => {
    it('[6] happy path: admin → 200 + 完整 checkpoint 对象', async () => {
      const cp = await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.data).toMatchObject({
        id: cp.id,
        name: cp.name,
        timezone: 'Asia/Shanghai',
      });
    });

    it('[7] 无 token → 401', async () => {
      const cp = await createCheckpoint();

      await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .expect(401);
    });

    it('[8] 普通员工 token → 403', async () => {
      const cp = await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${userToken}`);

      expect(resp.status).toBe(403);
    });

    it('[9] 不存在的 id → 404 + CHECKPOINT_NOT_FOUND', async () => {
      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/non-existent-id-000')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);

      const code =
        resp.body.data?.code ??
        resp.body.error?.code ??
        resp.body.code;
      expect(code).toBe('CHECKPOINT_NOT_FOUND');
    });
  });

  // ============================================================
  // PATCH /site-attendance/checkpoints/:id  (update)
  // ============================================================

  describe('PATCH /checkpoints/:id', () => {
    it('[10] happy path: admin 更新名称 → 200 + DB 已更新', async () => {
      const cp = await createCheckpoint();
      const newName = `t_CP_updated_${suffix()}`;

      const resp = await request(app.getHttpServer())
        .patch(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ name: newName })
        .expect(200);

      expect(resp.body.data.name).toBe(newName);

      // 验证 DB 已持久化
      const dbRow = await prisma.siteCheckpoint.findUnique({
        where: { id: cp.id },
      });
      expect(dbRow?.name).toBe(newName);
    });

    it('[11] 更新 isActive=false（软停用）→ checkpoint 变为不可用', async () => {
      const cp = await createCheckpoint();

      await request(app.getHttpServer())
        .patch(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ isActive: false })
        .expect(200);

      const dbRow = await prisma.siteCheckpoint.findUnique({
        where: { id: cp.id },
      });
      expect(dbRow?.isActive).toBe(false);
    });

    it('[12] 无 token → 401', async () => {
      const cp = await createCheckpoint();

      await request(app.getHttpServer())
        .patch(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .send({ name: 't_NewName' })
        .expect(401);
    });

    it('[13] 普通员工 token → 403', async () => {
      const cp = await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .patch(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${userToken}`)
        .send({ name: 't_NewName' });

      expect(resp.status).toBe(403);
    });

    it('[14] 不存在的 id → 404', async () => {
      await request(app.getHttpServer())
        .patch('/api/v1/site-attendance/checkpoints/non-existent-id-000')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ name: 't_Updated' })
        .expect(404);
    });

    it('[15] geoRadius < 100 → 400 must be at least 100', async () => {
      const cp = await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .patch(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ geoRadius: 50 })
        .expect(400);

      // 可能来自 DTO 校验 (@Min(100)) 或 service 校验，均为 400
      expect(resp.status).toBe(400);
    });

    it('[16] v1.5 sharedCheckinEnabled=true 但缺 sharedCompanyId → 400', async () => {
      const cp = await createCheckpoint();

      await request(app.getHttpServer())
        .patch(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ sharedCheckinEnabled: true, sharedCompanyLabel: 'Partner Co' })
        .expect(400);
    });

    it('[17] v1.5 qrRotationSeconds < 60 → 400', async () => {
      const cp = await createCheckpoint();

      await request(app.getHttpServer())
        .patch(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ qrRotationSeconds: 30 })
        .expect(400);
    });
  });

  // ============================================================
  // DELETE /site-attendance/checkpoints/:id
  // ============================================================

  describe('DELETE /checkpoints/:id', () => {
    it('[18] happy path: admin 删除 → 200 + message + DB 已删除', async () => {
      const cp = await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .delete(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.data.message).toBe('Checkpoint deleted');

      // 验证 DB 已删除
      const dbRow = await prisma.siteCheckpoint.findUnique({
        where: { id: cp.id },
      });
      expect(dbRow).toBeNull();
    });

    it('[19] 无 token → 401', async () => {
      const cp = await createCheckpoint();

      await request(app.getHttpServer())
        .delete(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .expect(401);
    });

    it('[20] 普通员工 token → 403', async () => {
      const cp = await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .delete(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${userToken}`);

      expect(resp.status).toBe(403);
    });

    it('[21] 不存在的 id → 404', async () => {
      await request(app.getHttpServer())
        .delete('/api/v1/site-attendance/checkpoints/non-existent-id-000')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);
    });

    it('[22] 幂等性：再次删除同一 id → 404', async () => {
      const cp = await createCheckpoint();

      // 第一次删除成功
      await request(app.getHttpServer())
        .delete(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      // 第二次应 404
      await request(app.getHttpServer())
        .delete(`/api/v1/site-attendance/checkpoints/${cp.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);
    });
  });

  // ============================================================
  // GET /site-attendance/checkpoints/:id/events  (events list)
  // ============================================================

  describe('GET /checkpoints/:id/events', () => {
    // 今日 localDate（Asia/Shanghai，时区偏移 +8 固定不变）
    function todayLocalDate(): string {
      const now = new Date();
      const s = now.toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' });
      return s; // YYYY-MM-DD
    }

    it('[23] happy path: admin → 200 + events + pagination', async () => {
      const cp = await createCheckpoint();
      const ld = todayLocalDate();
      await injectEvent(cp.id, userId, 'CHECK_IN', ld);

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}/events`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.data).toHaveProperty('events');
      expect(resp.body.data).toHaveProperty('pagination');
      expect(resp.body.data.events.length).toBeGreaterThanOrEqual(1);
      expect(resp.body.data.pagination).toMatchObject({
        page: 1,
        pageSize: expect.any(Number),
        total: expect.any(Number),
      });
    });

    it('[24] event 对象包含必要字段', async () => {
      const cp = await createCheckpoint();
      const ld = todayLocalDate();
      await injectEvent(cp.id, userId, 'CHECK_IN', ld);

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}/events`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      const ev = resp.body.data.events[0];
      expect(ev).toMatchObject({
        id: expect.any(String),
        userId: expect.any(String),
        eventType: 'CHECK_IN',
        timestamp: expect.any(String),
        authMethod: 'AUTHENTICATED',
        geoStatus: 'SKIPPED',
      });
      // resolvedAddress 字段存在（可为 null 或 string）
      expect(ev).toHaveProperty('resolvedAddress');
    });

    it('[25] 无 token → 401', async () => {
      const cp = await createCheckpoint();

      await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}/events`)
        .expect(401);
    });

    it('[26] 普通员工 token（无 records:read 权限）→ 403', async () => {
      const cp = await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}/events`)
        .set('Authorization', `Bearer ${userToken}`);

      expect(resp.status).toBe(403);
    });

    it('[27] 不存在的 checkpoint id → 404', async () => {
      await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/non-existent-id-000/events')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);
    });

    it('[28] date 过滤：指定昨日 date → 今日事件不出现', async () => {
      const cp = await createCheckpoint();
      const ld = todayLocalDate();
      await injectEvent(cp.id, userId, 'CHECK_IN', ld);

      // 查昨天
      const yesterday = new Date(Date.now() - 24 * 3600 * 1000)
        .toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}/events`)
        .query({ date: yesterday })
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.data.events).toHaveLength(0);
    });

    it('[29] eventType 过滤 CHECK_OUT → 只返回 CHECK_OUT 事件', async () => {
      const cp = await createCheckpoint();
      const ld = todayLocalDate();
      // 注入一对 IN+OUT
      await injectEvent(cp.id, userId, 'CHECK_IN', ld, 120_000);
      await injectEvent(cp.id, userId, 'CHECK_OUT', ld, 60_000);

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}/events`)
        .query({ eventType: 'CHECK_OUT' })
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      for (const ev of resp.body.data.events) {
        expect(ev.eventType).toBe('CHECK_OUT');
      }
    });

    it('[30] userName 过滤：搜索不存在的名字 → events 为空', async () => {
      const cp = await createCheckpoint();
      const ld = todayLocalDate();
      await injectEvent(cp.id, userId, 'CHECK_IN', ld);

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}/events`)
        .query({ userName: 'xxxxxxxxxnotexists999' })
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.data.events).toHaveLength(0);
    });

    it('[31] 分页：page=1&pageSize=1 → 最多 1 条事件', async () => {
      const cp = await createCheckpoint();
      const ld = todayLocalDate();
      await injectEvent(cp.id, userId, 'CHECK_IN', ld, 180_000);
      // 注入第 2 条（先 CHECK_OUT 再下一组 CHECK_IN 不合法，直接注入两个 CHECK_IN 不同用户）
      const s = suffix();
      const anotherUser = await createTestUser({
        username: `t_cp_u2_${s}`,
        email: `t_cp_u2_${s}@example.com`,
        password: 'User@1234',
        displayName: `t_CP User2 ${s}`,
      });
      await injectEvent(cp.id, anotherUser.id, 'CHECK_IN', ld, 90_000);

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}/events`)
        .query({ page: 1, pageSize: 1 })
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.data.events).toHaveLength(1);
      expect(resp.body.data.pagination.pageSize).toBe(1);
      expect(resp.body.data.pagination.total).toBeGreaterThanOrEqual(2);
    });

    it('[32] 空 checkpoint（无事件）→ events=[] + pagination.total=0', async () => {
      const cp = await createCheckpoint();

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/${cp.id}/events`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.data.events).toHaveLength(0);
      expect(resp.body.data.pagination.total).toBe(0);
    });
  });

  // ============================================================
  // POST /checkpoints （dedicated coverage）
  // ============================================================
  // 注：本 endpoint 在 createCheckpoint helper 里被 fixture 隐式调用 30+ 次（happy path
  // 已被反复验证）。本块补 dedicated case 锚定 auth + DTO 校验边界，符合 #341 模块
  // 严格 100% 标准（每 endpoint ≥ happy + auth + 业务规则/边界）。
  describe('POST /checkpoints (dedicated)', () => {
    it('[33] admin happy path → 201 + 返回 checkpoint + DB 写入', async () => {
      const s = suffix();
      const payload = {
        name: `t_dedicated_${s}`,
        timezone: 'Asia/Shanghai',
        latitude: 39.9042,
        longitude: 116.4074,
        accessMode: 'PUBLIC',
        geoPolicy: 'SKIP',
      };

      const resp = await request(app.getHttpServer())
        .post('/api/v1/site-attendance/checkpoints')
        .set('Authorization', `Bearer ${adminToken}`)
        .send(payload)
        .expect(201);

      expect(resp.body.data).toHaveProperty('id');
      expect(resp.body.data.name).toBe(payload.name);
      expect(resp.body.data.timezone).toBe(payload.timezone);

      // DB 写入验证（Prisma model = SiteCheckpoint，@@map "site_checkpoints"）
      const dbRow = await prisma.siteCheckpoint.findUnique({
        where: { id: resp.body.data.id },
      });
      expect(dbRow).toBeTruthy();
      expect(dbRow!.name).toBe(payload.name);
    });

    it('[34] 无 token → 401', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/site-attendance/checkpoints')
        .send({
          name: `t_no_token_${suffix()}`,
          timezone: 'Asia/Shanghai',
          latitude: 0,
          longitude: 0,
        })
        .expect(401);
    });

    it('[35] 普通员工 token（无 site-attendance:checkpoint:manage permission）→ 403', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/site-attendance/checkpoints')
        .set('Authorization', `Bearer ${userToken}`)
        .send({
          name: `t_emp_${suffix()}`,
          timezone: 'Asia/Shanghai',
          latitude: 0,
          longitude: 0,
        })
        .expect(403);
    });

    it('[36] DTO 必填字段缺（name 缺）→ 400 VALIDATION_ERROR', async () => {
      const resp = await request(app.getHttpServer())
        .post('/api/v1/site-attendance/checkpoints')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          // name 缺
          timezone: 'Asia/Shanghai',
          latitude: 39.9042,
          longitude: 116.4074,
        })
        .expect(400);

      expect(resp.body.error?.code ?? resp.body.code).toBe('VALIDATION_ERROR');
    });
  });
});
