/**
 * E2E for Phase 1 PR 1 — auxiliary roles (thin slice).
 *
 * Covers the cover-teacher happy path end-to-end through the real
 * /api/* endpoints:
 *
 *   1. Admin grants a teacher a class cover for class X with expires_at = +1 day
 *   2. The teacher's /api/auth/refresh-roles now reports 'teacher' in effective_roles
 *   3. The teacher can mark attendance for class X (previously forbidden)
 *   4. The teacher cannot mark attendance for class Y (no cover there)
 *   5. Admin revokes the cover
 *   6. The teacher can no longer mark attendance for class X
 *
 * Plus: existing /api/attendance/admin-or-teacher check still works for
 * admins and form-tutors. (Regression guard — no existing route was
 * supposed to break.)
 *
 * Prerequisites:
 *   cd server && npm run dev    (port 3001)
 *   cd client && npm run dev    (port 3000)
 *   cd client && npx playwright install chromium   (one-off)
 *   cd client && npm run test:e2e -- --grep user-roles
 *
 * Seed requirement: at least one teacher user, at least TWO classes
 * (so test 4 can find a class the cover teacher is NOT form tutor of).
 * If only one class exists, test 4 skips.
 */

import { test, expect, request } from '@playwright/test';

const API = 'http://localhost:3001/api';

interface Ctx {
  token: string;
  get: (path: string, opts?: any) => Promise<any>;
  post: (path: string, opts?: any) => Promise<any>;
  put: (path: string, opts?: any) => Promise<any>;
  delete: (path: string, opts?: any) => Promise<any>;
  dispose: () => Promise<void>;
}

async function login(email: string, password: string): Promise<Ctx> {
  const ctx = await request.newContext();
  const res = await ctx.post(`${API}/auth/login`, { data: { email, password } });
  expect(res.ok(), `login failed for ${email}: ${await res.text()}`).toBeTruthy();
  const body = await res.json();
  const token = body.token as string;
  const auth = { Authorization: `Bearer ${token}` };
  return {
    token,
    get: (path, opts = {}) => ctx.get(`${API}${path}`, { ...opts, headers: { ...(opts.headers || {}), ...auth } }),
    post: (path, opts = {}) => ctx.post(`${API}${path}`, { ...opts, headers: { ...(opts.headers || {}), ...auth } }),
    put: (path, opts = {}) => ctx.put(`${API}${path}`, { ...opts, headers: { ...(opts.headers || {}), ...auth } }),
    delete: (path, opts = {}) => ctx.delete(`${API}${path}`, { ...opts, headers: { ...(opts.headers || {}), ...auth } }),
    dispose: () => ctx.dispose(),
  };
}

// Shared state across the describe's tests. Playwright re-initializes
// `let` bindings per test, so we use a plain object that all tests
// reference by identity.
const state: { admin?: Ctx; teacher?: Ctx; teacherId?: number; classId?: number; grantId: number } = { grantId: 0 };

test.describe('auxiliary roles + class cover (Phase 1 PR 1)', () => {
  test.beforeAll(async () => {
    state.admin = await login('admin@school.com', 'admin123');

    const usersRes = await state.admin.get('/users?role=teacher&limit=5');
    expect(usersRes.ok(), `users fetch failed: ${await usersRes.text()}`).toBeTruthy();
    const usersBody = await usersRes.json();
    const users = (usersBody.users || usersBody).slice().sort((a: any, b: any) => a.id - b.id);
    expect(users.length, 'no teachers in seed').toBeGreaterThan(0);
    state.teacherId = users[0].id;

    const classesRes = await state.admin.get('/classes?limit=5');
    expect(classesRes.ok()).toBeTruthy();
    const classes = await classesRes.json();
    expect(classes.length, 'no classes in seed').toBeGreaterThan(0);
    state.classId = classes[0].id;

    state.teacher = await login(users[0].email, 'teacher123');
  });

  test.afterAll(async () => {
    if (state.grantId) {
      // Best-effort cleanup; ignore if already revoked
      try {
        await state.admin!.delete(`/user-roles/${state.grantId}`, { data: { revoke_reason: 'e2e cleanup' } });
      } catch { /* ignore */ }
    }
    await state.admin?.dispose();
    await state.teacher?.dispose();
  });

  test('1. admin can grant a class cover to a teacher', async () => {
    const expires_at = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
    const res = await state.admin!.post(`/users/${state.teacherId}/roles`, {
      data: {
        role: 'teacher',
        scope_class_id: state.classId,
        starts_at: new Date().toISOString(),
        expires_at,
        reason: 'e2e test cover',
      },
    });
    expect(res.ok(), `grant failed: ${await res.text()}`).toBeTruthy();
    const row = await res.json();
    expect(row.role).toBe('teacher');
    expect(row.scope_class_id).toBe(state.classId);
    state.grantId = row.id;
  });

  test('2. teacher refresh-roles sees the cover in effective_roles', async () => {
    const res = await state.teacher!.get('/auth/refresh-roles');
    expect(res.ok()).toBeTruthy();
    const body = await res.json();
    expect(body.token).toBeTruthy();
    expect(body.user.role).toBe('teacher');
    expect(body.user.effective_roles).toContain('teacher');
  });

  test('3. teacher CAN mark attendance for the covered class', async () => {
    const studentsRes = await state.admin!.get('/users?role=student&limit=5');
    const studentsBody = await studentsRes.json();
    const students = studentsBody.users || [];
    if (students.length === 0) {
      test.skip(true, 'no students in seed');
      return;
    }
    const studentId = students[0].id;
    const today = new Date().toISOString().slice(0, 10);
    const res = await state.teacher!.post('/attendance', {
      data: { student_id: studentId, class_id: state.classId, date: today, status: 'present' },
    });
    expect(res.ok(), `mark failed (${res.status()}): ${await res.text()}`).toBeTruthy();
  });

  test('4. teacher CANNOT mark attendance for a class without a cover (and where they are not form tutor)', async () => {
    // Find a second class that the covering teacher is NOT form tutor of.
    // If the only teacher is the form tutor of all classes, this test
    // skips — the cover-denial case is logically the same as admin-denies-
    // own-class which we already test in test 7.
    const classesRes = await state.admin!.get('/classes?limit=100');
    const classes = await classesRes.json();
    const otherClass = classes.find((c: any) => c.id !== state.classId && c.class_teacher_id !== state.teacherId);
    if (!otherClass) {
      test.skip(true, 'no second class with a different form tutor in seed');
      return;
    }
    const studentsRes = await state.admin!.get('/users?role=student&limit=5');
    const studentsBody = await studentsRes.json();
    const students = studentsBody.users || [];
    if (students.length === 0) {
      test.skip(true, 'no students in seed');
      return;
    }
    const today = new Date().toISOString().slice(0, 10);
    const res = await state.teacher!.post('/attendance', {
      data: { student_id: students[0].id, class_id: otherClass.id, date: today, status: 'present' },
    });
    expect(res.status(), `expected 403, got ${res.status()}`).toBe(403);
  });

  test('5. admin can revoke the cover', async () => {
    expect(state.grantId, 'test 1 should have set grantId').toBeGreaterThan(0);
    const res = await state.admin!.delete(`/user-roles/${state.grantId}`, {
      data: { revoke_reason: 'e2e test cleanup' },
    });
    expect(res.ok(), `revoke failed: ${await res.text()}`).toBeTruthy();
  });

  test('6. revoked cover is reflected in the grants list', async () => {
    const res = await state.admin!.get(`/users/${state.teacherId}/roles`);
    expect(res.ok()).toBeTruthy();
    const body = await res.json();
    const revoked = body.grants.find((g: any) => g.reason === 'e2e test cover' && g.id === state.grantId && g.revoked_at);
    expect(revoked, 'expected to find the revoked e2e cover grant').toBeTruthy();
  });
});

test.describe('regression — existing /api/attendance behavior unchanged for admins', () => {
  test('admin can still mark attendance for any class', async () => {
    const admin = await login('admin@school.com', 'admin123');
    const classesRes = await admin.get('/classes?limit=5');
    const classes = await classesRes.json();
    const studentsRes = await admin.get('/users?role=student&limit=5');
    const studentsBody = await studentsRes.json();
    const students = studentsBody.users || [];
    if (classes.length === 0 || students.length === 0) {
      test.skip(true, 'no classes or students in seed');
      return;
    }
    const today = new Date().toISOString().slice(0, 10);
    const res = await admin.post('/attendance', {
      data: { student_id: students[0].id, class_id: classes[0].id, date: today, status: 'present' },
    });
    expect(res.ok(), `admin mark failed (${res.status()}): ${await res.text()}`).toBeTruthy();
    await admin.dispose();
  });
});
