/**
 * L1 集成测试 — site-attendance checkin & today-duration 基线
 *
 * 覆盖：
 *   - POST /checkin（已登录）契约 + todayDurationSeconds 字段返回
 *   - POST /guest-checkin（PUBLIC）契约 + 多段累加
 *   - GET /today-events 包含 todayDurationSeconds
 *   - GET /today-summary（管理端）含 totalDurationSeconds / avgDurationSeconds
 *   - getLocalDate 5am workday 边界（单元级）
 *
 * 多段累加通过直接 prisma 注入历史事件 + 手动 upsert summary 模拟，
 * 避免依赖真实时间间隔与 30s 防重保护。
 */

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

describe('Site Attendance Checkin & Duration - L1', () => {
  let app: INestApplication;
  let prisma: PrismaService;
  let adminToken: string;
  let userToken: string;
  let userId: string;

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

  beforeEach(async () => {
    const suffix = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
    const admin = await createAdminUser({
      username: `t_dur_admin_${suffix}`,
      email: `t_dur_admin_${suffix}@example.com`,
      password: 'Admin@123',
      displayName: 't_Duration Admin',
    });
    const user = await createTestUser({
      username: `t_dur_user_${suffix}`,
      email: `t_dur_user_${suffix}@example.com`,
      password: 'User@1234',
      displayName: 't_Duration User',
    });
    userId = user.id;

    const adminLogin = await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({ username: admin.username, password: 'Admin@123' })
      .expect(200);
    adminToken = adminLogin.body.data.accessToken as string;

    const userLogin = await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({ username: user.username, password: 'User@1234' })
      .expect(200);
    userToken = userLogin.body.data.accessToken as string;
  });

  afterEach(async () => {
    jest.restoreAllMocks();
    // issue #165 PR b: 切到前缀过滤 cleanup（schema 无关，零维护负担）
    // 测试 fixture 都用 `t_` 前缀；TEST_ADMIN_ORG 等 factory 共享种子无前缀，不被清
    await cleanupByPrefix(prisma);
  });

  afterAll(async () => {
    await app.close();
  });

  async function createCheckpoint(overrides: Partial<Record<string, any>> = {}) {
    const name = `t_CP-${Math.random().toString(36).slice(2, 8)}`;
    const res = await request(app.getHttpServer())
      .post('/api/v1/site-attendance/checkpoints')
      .set('Authorization', `Bearer ${adminToken}`)
      .send({
        name,
        timezone: 'America/Los_Angeles',
        latitude: 34.05,
        longitude: -118.24,
        accessMode: 'PUBLIC',
        geoPolicy: 'SKIP',
        ...overrides,
      })
      .expect(201);
    return res.body.data;
  }

  it('today-events on empty → todayDurationSeconds = 0', async () => {
    const cp = await createCheckpoint();
    const res = await request(app.getHttpServer())
      .get(`/api/v1/site-attendance/checkpoints/code/${cp.code}/today-events`)
      .query({ userId })
      .expect(200);

    expect(res.body.data.summary.todayDurationSeconds).toBe(0);
    expect(res.body.data.events).toHaveLength(0);
  });

  it('CHECK_IN response carries todayDurationSeconds (open segment ≥ 0)', async () => {
    const cp = await createCheckpoint();
    const res = await request(app.getHttpServer())
      .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/checkin`)
      .set('Authorization', `Bearer ${userToken}`)
      .send({ eventType: 'CHECK_IN', geoStatus: 'SKIPPED' })
      .expect(201);

    expect(typeof res.body.data.todaySummary.todayDurationSeconds).toBe(
      'number',
    );
    expect(res.body.data.todaySummary.todayDurationSeconds).toBeGreaterThanOrEqual(
      0,
    );
    expect(res.body.data.todaySummary.todayDurationSeconds).toBeLessThan(5);
  });

  it('CHECK_IN(t-2h) + CHECK_OUT(t-1h) injected → duration ≈ 3600s', async () => {
    const cp = await createCheckpoint();
    const now = new Date();
    const localDate = getLocalDate(cp.timezone, now);
    const t2hAgo = new Date(now.getTime() - 2 * 3600 * 1000);
    const t1hAgo = new Date(now.getTime() - 1 * 3600 * 1000);

    await prisma.siteAttendanceEvent.create({
      data: {
        checkpointId: cp.id,
        userId,
        eventType: 'CHECK_IN',
        timestamp: t2hAgo,
        localDate,
        authMethod: 'AUTHENTICATED',
        geoStatus: 'SKIPPED',
      },
    });
    await prisma.siteAttendanceEvent.create({
      data: {
        checkpointId: cp.id,
        userId,
        eventType: 'CHECK_OUT',
        timestamp: t1hAgo,
        localDate,
        authMethod: 'AUTHENTICATED',
        geoStatus: 'SKIPPED',
      },
    });

    const res = await request(app.getHttpServer())
      .get(`/api/v1/site-attendance/checkpoints/code/${cp.code}/today-events`)
      .query({ userId })
      .expect(200);

    const dur = res.body.data.summary.todayDurationSeconds;
    expect(dur).toBeGreaterThanOrEqual(3600 - 5);
    expect(dur).toBeLessThanOrEqual(3600 + 5);
  });

  it('two closed segments accumulate (1h + 30m = 5400s)', async () => {
    const cp = await createCheckpoint();
    const now = new Date();
    const localDate = getLocalDate(cp.timezone, now);
    const ts = (mins: number) => new Date(now.getTime() - mins * 60 * 1000);

    const inserts = [
      { eventType: 'CHECK_IN', timestamp: ts(300) },   // 5h ago
      { eventType: 'CHECK_OUT', timestamp: ts(240) },  // 4h ago  → 1h
      { eventType: 'CHECK_IN', timestamp: ts(120) },   // 2h ago
      { eventType: 'CHECK_OUT', timestamp: ts(90) },   // 1.5h ago → 30m
    ];
    for (const e of inserts) {
      await prisma.siteAttendanceEvent.create({
        data: {
          checkpointId: cp.id,
          userId,
          ...e,
          eventType: e.eventType as any,
          localDate,
          authMethod: 'AUTHENTICATED',
          geoStatus: 'SKIPPED',
        },
      });
    }

    const res = await request(app.getHttpServer())
      .get(`/api/v1/site-attendance/checkpoints/code/${cp.code}/today-events`)
      .query({ userId })
      .expect(200);

    const dur = res.body.data.summary.todayDurationSeconds;
    expect(dur).toBeGreaterThanOrEqual(5400 - 5);
    expect(dur).toBeLessThanOrEqual(5400 + 5);
  });

  it('admin today-summary aggregates totalDurationSeconds & avgDurationSeconds', async () => {
    const cp = await createCheckpoint();
    const now = new Date();
    const localDate = getLocalDate(cp.timezone, now);

    // user A: 1h closed + summary upsert
    await prisma.siteAttendanceEvent.create({
      data: {
        checkpointId: cp.id, userId,
        eventType: 'CHECK_IN',
        timestamp: new Date(now.getTime() - 2 * 3600 * 1000),
        localDate, authMethod: 'AUTHENTICATED', geoStatus: 'SKIPPED',
      },
    });
    await prisma.siteAttendanceEvent.create({
      data: {
        checkpointId: cp.id, userId,
        eventType: 'CHECK_OUT',
        timestamp: new Date(now.getTime() - 1 * 3600 * 1000),
        localDate, authMethod: 'AUTHENTICATED', geoStatus: 'SKIPPED',
      },
    });
    await prisma.siteDailySummary.create({
      data: {
        checkpointId: cp.id, userId, localDate,
        firstCheckInAt: new Date(now.getTime() - 2 * 3600 * 1000),
        lastCheckOutAt: new Date(now.getTime() - 1 * 3600 * 1000),
        checkInCount: 1, checkOutCount: 1,
      },
    });

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

    expect(res.body.data.totalDurationSeconds).toBeGreaterThanOrEqual(3600 - 5);
    expect(res.body.data.totalDurationSeconds).toBeLessThanOrEqual(3600 + 5);
    expect(res.body.data.avgDurationSeconds).toBeGreaterThanOrEqual(3600 - 5);
    expect(res.body.data.avgDurationSeconds).toBeLessThanOrEqual(3600 + 5);
  });

  // ----- 5am workday boundary -----
  it('getLocalDate: 04:30 local → previous calendar date (5am rule)', () => {
    const tz = 'America/Los_Angeles';
    // 2026-04-29 04:30 LA local = 2026-04-29 11:30 UTC
    const at0430 = new Date('2026-04-29T11:30:00Z');
    expect(getLocalDate(tz, at0430)).toBe('2026-04-28');
  });

  it('getLocalDate: 05:30 local → same calendar date (after rollover)', () => {
    const tz = 'America/Los_Angeles';
    // 2026-04-29 05:30 LA local = 2026-04-29 12:30 UTC
    const at0530 = new Date('2026-04-29T12:30:00Z');
    expect(getLocalDate(tz, at0530)).toBe('2026-04-29');
  });

  it('WORKDAY_START_HOUR is exposed for cross-module assumptions', () => {
    expect(WORKDAY_START_HOUR).toBe(5);
  });

  // ----- 多周期签到 vs 30s 防重 互动（ERR-20260429-011） -----
  it('rapid IN→OUT→IN within 30s is allowed (multi-cycle)', async () => {
    const cp = await createCheckpoint();
    const post = (eventType: 'CHECK_IN' | 'CHECK_OUT') =>
      request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/checkin`)
        .set('Authorization', `Bearer ${userToken}`)
        .send({ eventType, geoStatus: 'SKIPPED' });
    await post('CHECK_IN').expect(201);
    await post('CHECK_OUT').expect(201);
    // 第三步：30s 内再次 CHECK_IN — 之前实现错杀（30s 内有 CHECK_IN 即 dup）
    const third = await post('CHECK_IN').expect(201);
    expect(third.body.data.event.eventType).toBe('CHECK_IN');
  });

  it('same-type consecutive submission rejected (validateEventOrder defense)', async () => {
    const cp = await createCheckpoint();
    const post = () =>
      request(app.getHttpServer())
        .post(`/api/v1/site-attendance/checkpoints/code/${cp.code}/checkin`)
        .set('Authorization', `Bearer ${userToken}`)
        .send({ eventType: 'CHECK_IN', geoStatus: 'SKIPPED' });
    await post().expect(201);
    const dup = await post().expect(400);
    expect(dup.body.error?.code ?? dup.body.code).toBe('ALREADY_CHECKED_IN');
  });
});
