/**
 * Site-attendance · Checkin Controller L1 集成测试（缺口端点）
 *
 * 覆盖 checkin.controller 中 4 个尚未被 checkin-duration.api.test.ts 覆盖的端点：
 *   GET  /site-attendance/checkpoints/code/:code/user-search   （@Public）
 *   GET  /site-attendance/checkpoints/code/geocode/search      （@Public）
 *   GET  /site-attendance/checkpoints/code/geocode/reverse     （@Public）
 *   POST /site-attendance/checkpoints/code/:code/guest-checkin （@Public）
 *
 * 不重复测 checkin-duration.api.test.ts 已覆盖的：
 *   GET  /code/:code/public  / GET /today-events  /
 *   POST /:code/checkin      / GET /today-summary
 *
 * Geocoding service 调外部 nominatim API → jest.spyOn mock，避免真实网络请求。
 *
 * 关联工单 #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 · Checkin 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;
  }

  /** 创建管理员 + 普通测试用户，返回 tokens */
  async function setupUsers() {
    const s = suffix();
    const admin = await createAdminUser({
      username: `t_sa_adm_${s}`,
      email: `t_sa_adm_${s}@example.com`,
      password: 'Admin@123',
      displayName: `t_SA Admin ${s}`,
    });
    const user = await createTestUser({
      username: `t_sa_usr_${s}`,
      email: `t_sa_usr_${s}@example.com`,
      password: 'User@1234',
      displayName: `t_SA User ${s}`,
    });
    userId = user.id;

    const at = await login(admin.username, 'Admin@123');
    adminToken = at;

    const ut = await login(user.username, 'User@1234');
    userToken = ut;

    return { admin, adminToken: at, user, userToken: ut };
  }

  /** 通过 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',
        allowUnauthenticatedCheckin: true,
        ...overrides,
      })
      .expect(201);
    return res.body.data;
  }

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

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

    // 预先建一套全局 users（大多数 describe block 共用）
    await setupUsers();
  });

  beforeEach(async () => {
    // 仅恢复 mock，不重建 users（避免每 case 都 bcrypt）
    jest.restoreAllMocks();
  });

  afterEach(async () => {
    // 清理本次测试写入的 site-attendance 事件 / checkpoint 数据
    // cleanupByPrefix 会扫 platform_site_attendance schema 匹配 t_ 前缀的行
    await prisma.siteAttendanceEvent.deleteMany({}).catch((e: unknown) =>
      console.warn('afterEach siteAttendanceEvent cleanup warn:', e),
    );
    await prisma.siteDailySummary.deleteMany({}).catch((e: unknown) =>
      console.warn('afterEach siteDailySummary cleanup warn:', e),
    );
    await prisma.siteCheckpoint.deleteMany({
      where: { name: { startsWith: 't_' } },
    }).catch((e: unknown) =>
      console.warn('afterEach siteCheckpoint 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/code/:code/user-search
  // ============================================================

  describe('GET /checkpoints/code/:code/user-search', () => {
    it('[1] happy path: 匹配 displayName → 返回 users 数组', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/code/${cp.code}/user-search`)
        .query({ q: 't_SA' }) // 至少 3 字符，且能匹配 beforeAll 创建的 t_SA User
        .expect(200);

      expect(resp.body.data).toHaveProperty('users');
      expect(Array.isArray(resp.body.data.users)).toBe(true);
      // 断言每条记录结构
      for (const u of resp.body.data.users) {
        expect(u).toHaveProperty('id');
        expect(u).toHaveProperty('displayName');
        expect(u).toHaveProperty('email');
        // department 可为 null，但字段必须存在
        expect(u).toHaveProperty('department');
      }
    });

    it('[2] @Public：无 Authorization 头也能访问（200 not 401）', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/code/${cp.code}/user-search`)
        .query({ q: 'abc' }); // 3 字符，允许无 token

      expect(resp.status).not.toBe(401);
    });

    it('[3] checkpoint 不存在 → 404', async () => {
      await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/NON_EXISTENT_CODE/user-search')
        .query({ q: 'abc' })
        .expect(404);
    });

    it('[4] checkpoint 不允许 unauthenticated checkin → 403', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: false });

      await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/code/${cp.code}/user-search`)
        .query({ q: 'abc' })
        .expect(403);
    });

    it('[5] query 少于 3 字符 → 400', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/code/${cp.code}/user-search`)
        .query({ q: 'ab' })
        .expect(400);
    });

    it('[6] query 为空字符串 → 400', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/code/${cp.code}/user-search`)
        .query({ q: '' })
        .expect(400);
    });

    it('[7] 无任何匹配 → 返回 { users: [] }', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/site-attendance/checkpoints/code/${cp.code}/user-search`)
        .query({ q: 'xxxxxxxxxnotexists999' })
        .expect(200);

      expect(resp.body.data.users).toEqual([]);
    });
  });

  // ============================================================
  // GET /site-attendance/checkpoints/code/geocode/search
  // ============================================================

  describe('GET /checkpoints/code/geocode/search', () => {
    it('[8] happy path: mock nominatim 返回地点列表', async () => {
      // mock GeocodingService.searchPlaces，避免真实外部 API 调用
      jest.spyOn(geocodingService, 'searchPlaces').mockResolvedValue({
        places: [
          {
            latitude: 39.9042,
            longitude: 116.4074,
            displayName: '天安门广场, 东城区, 北京',
            title: '天安门广场',
          },
        ],
      });

      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/search')
        .query({ q: '天安门广场' })
        .expect(200);

      expect(resp.body.data).toHaveProperty('places');
      expect(resp.body.data.places).toHaveLength(1);
      expect(resp.body.data.places[0]).toMatchObject({
        latitude: expect.any(Number),
        longitude: expect.any(Number),
        displayName: expect.any(String),
        title: expect.any(String),
      });
    });

    it('[9] @Public：无 Authorization 头也能访问', async () => {
      jest.spyOn(geocodingService, 'searchPlaces').mockResolvedValue({ places: [] });

      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/search')
        .query({ q: 'some place' });

      expect(resp.status).not.toBe(401);
    });

    it('[10] query 少于 2 字符 → 400', async () => {
      await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/search')
        .query({ q: 'a' })
        .expect(400);
    });

    it('[11] query 缺失 → 400', async () => {
      await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/search')
        .expect(400);
    });

    it('[12] 外部 API 无结果时返回 { places: [] }', async () => {
      jest.spyOn(geocodingService, 'searchPlaces').mockResolvedValue({ places: [] });

      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/search')
        .query({ q: '无匹配地点xyz' })
        .expect(200);

      expect(resp.body.data.places).toEqual([]);
    });

    it('[13] lang 参数可选传入（不影响 200 成功）', async () => {
      jest.spyOn(geocodingService, 'searchPlaces').mockResolvedValue({ places: [] });

      await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/search')
        .query({ q: 'beijing', lang: 'en' })
        .expect(200);
    });
  });

  // ============================================================
  // GET /site-attendance/checkpoints/code/geocode/reverse
  // ============================================================

  describe('GET /checkpoints/code/geocode/reverse', () => {
    it('[14] happy path: mock reverseGeocode 返回 displayName', async () => {
      jest.spyOn(geocodingService, 'reverseGeocode').mockResolvedValue({
        displayName: '北京市东城区天安门广场',
      });

      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/reverse')
        .query({ lat: '39.9042', lon: '116.4074' })
        .expect(200);

      expect(resp.body.data).toHaveProperty('displayName');
      expect(typeof resp.body.data.displayName).toBe('string');
    });

    it('[15] @Public：无 Authorization 头也能访问', async () => {
      jest.spyOn(geocodingService, 'reverseGeocode').mockResolvedValue({
        displayName: '某地',
      });

      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/reverse')
        .query({ lat: '39.9', lon: '116.4' });

      expect(resp.status).not.toBe(401);
    });

    it('[16] lat/lon 均为有效浮点数 → mock 被调用一次', async () => {
      const spy = jest.spyOn(geocodingService, 'reverseGeocode').mockResolvedValue({
        displayName: '测试地址',
      });

      await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/reverse')
        .query({ lat: '31.2304', lon: '121.4737' })
        .expect(200);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(31.2304, 121.4737, undefined);
    });

    it('[17] lang 参数透传给 service', async () => {
      const spy = jest.spyOn(geocodingService, 'reverseGeocode').mockResolvedValue({
        displayName: 'Shanghai',
      });

      await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/reverse')
        .query({ lat: '31.23', lon: '121.47', lang: 'en' })
        .expect(200);

      expect(spy).toHaveBeenCalledWith(31.23, 121.47, 'en');
    });

    it('[18] lat/lon 为 NaN（非数字字符串）→ service 内部返回 displayName 为空串（不 500）', async () => {
      // service 内部对 non-finite 直接返回 { displayName: '' }，不抛出
      jest.spyOn(geocodingService, 'reverseGeocode').mockResolvedValue({ displayName: '' });

      const resp = await request(app.getHttpServer())
        .get('/api/v1/site-attendance/checkpoints/code/geocode/reverse')
        .query({ lat: 'abc', lon: 'xyz' });

      // 不应 500，service 做了兜底
      expect(resp.status).not.toBe(500);
    });
  });

  // ============================================================
  // POST /site-attendance/checkpoints/code/:code/guest-checkin
  // ============================================================

  describe('POST /checkpoints/code/:code/guest-checkin', () => {
    it('[19] happy path: 嘉宾 CHECK_IN 成功 → 201 + event + todaySummary', async () => {
      const cp = await createCheckpoint({
        allowUnauthenticatedCheckin: true,
        geoPolicy: 'SKIP',
      });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/guest-checkin`)
        .send({
          userId,
          eventType: 'CHECK_IN',
          geoStatus: 'SKIPPED',
        })
        .expect(201);

      expect(resp.body.data).toHaveProperty('event');
      expect(resp.body.data.event.eventType).toBe('CHECK_IN');
      expect(resp.body.data).toHaveProperty('todaySummary');
      expect(resp.body.data.todaySummary).toHaveProperty('todayDurationSeconds');
    });

    it('[20] @Public：无 Authorization 头也能成功签到', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/guest-checkin`)
        // 故意不带 Authorization 头
        .send({ userId, eventType: 'CHECK_IN', geoStatus: 'SKIPPED' });

      expect(resp.status).not.toBe(401);
      expect(resp.status).toBe(201);
    });

    it('[21] authMethod 为 UNAUTHENTICATED：DB 行 authMethod 字段正确', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      await request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/guest-checkin`)
        .send({ userId, eventType: 'CHECK_IN', geoStatus: 'SKIPPED' })
        .expect(201);

      const event = await prisma.siteAttendanceEvent.findFirst({
        where: { checkpointId: cp.id, userId },
        orderBy: { timestamp: 'desc' },
      });
      expect(event?.authMethod).toBe('UNAUTHENTICATED');
    });

    it('[22] checkpoint 不允许 unauthenticated → 403', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: false });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/guest-checkin`)
        .send({ userId, eventType: 'CHECK_IN', geoStatus: 'SKIPPED' });

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

    it('[23] checkpoint 不存在 → 404', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/site-attendance/checkpoints/code/NON_EXISTENT_CODE/guest-checkin')
        .send({ userId, eventType: 'CHECK_IN', geoStatus: 'SKIPPED' })
        .expect(404);
    });

    it('[24] userId 不存在 → 404', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      // 注意：用合法 UUID 走 service 的"用户不存在 → 404"路径。
      // 用非 UUID 字符串会触发 Prisma P2023，落入跟工单 #463 同根因的
      // controller handleError 未兜底 → 500 而非 400/404（待 #463 修复）。
      await request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/guest-checkin`)
        .send({
          userId: '00000000-0000-0000-0000-000000000000',
          eventType: 'CHECK_IN',
          geoStatus: 'SKIPPED',
        })
        .expect(404);
    });

    it('[25] CHECK_OUT without prior CHECK_IN → 400 MUST_CHECK_IN_FIRST', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/guest-checkin`)
        .send({ userId, eventType: 'CHECK_OUT', geoStatus: 'SKIPPED' })
        .expect(400);

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

    it('[26] eventType 非法值 → 400 DTO 校验失败', async () => {
      const cp = await createCheckpoint({ allowUnauthenticatedCheckin: true });

      await request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/guest-checkin`)
        .send({ userId, eventType: 'INVALID_TYPE', geoStatus: 'SKIPPED' })
        .expect(400);
    });

    it('[27] geoPolicy=STRICT_BLOCK + client geoStatus=PERMISSION_DENIED → 403', async () => {
      const cp = await createCheckpoint({
        allowUnauthenticatedCheckin: true,
        geoPolicy: 'STRICT_BLOCK',
        latitude: 39.9042,
        longitude: 116.4074,
        geoRadius: 200,
      });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/guest-checkin`)
        .send({
          userId,
          eventType: 'CHECK_IN',
          geoStatus: 'PERMISSION_DENIED',
        });

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