/**
 * Exam-review workflow e2e (added 2026-07-21).
 *
 * Walks the full review pipeline through the HTTP layer:
 *
 *   1. Teacher creates a draft exam group              (POST /api/exams/groups)
 *   2. Teacher submits it for review                   (POST /api/exams/groups/:id/submit)
 *   3. Admin sees it in the pending queue              (GET  /api/exams/groups/pending)
 *   4. Admin rejects with a reason                     (POST /api/exams/groups/:id/reject)
 *   5. Teacher sees the rejected status + reason on the group
 *   6. Teacher resubmits                               (POST /api/exams/groups/:id/submit)
 *   7. Admin creates a draft schedule                  (POST /api/exams/schedules)
 *   8. Admin publishes (flips is_active + approves)   (POST /api/exams/schedules/:id/publish)
 *   9. Group is now approved
 *
 * Plus RBAC checks at each gate:
 *   - student gets 403 on admin endpoints
 *   - teacher gets 403 on admin endpoints
 *   - anonymous gets 401 on everything
 *
 * Uses request contexts directly so the test is fast (no browser
 * round-trip) and asserts the exact HTTP contract.
 */
import { test, expect, request as pwRequest } from '@playwright/test';

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

const USERS = {
  admin:   { email: 'admin@school.com',   password: 'admin123' },
  teacher: { email: 'teacher@school.com', password: 'teacher123' },
  student: { email: 'student@school.com', password: 'student123' },
};

async function login(role) {
  const ctx = await pwRequest.newContext({ baseURL: API });
  const res = await ctx.post('/api/auth/login', { data: USERS[role] });
  if (res.status() !== 200) {
    await ctx.dispose();
    throw new Error(`login failed for ${role}: ${res.status()} ${await res.text()}`);
  }
  const body = await res.json();
  return { ctx, token: body.token, user: body.user };
}

async function get(token, path) {
  const ctx = await pwRequest.newContext({ baseURL: API });
  const res = await ctx.get(path, { headers: { Authorization: `Bearer ${token}` } });
  const text = await res.text();
  await ctx.dispose();
  return { status: res.status(), body: safeParse(text) };
}

async function post(token, path, data) {
  const ctx = await pwRequest.newContext({ baseURL: API });
  const res = await ctx.post(path, {
    headers: { Authorization: `Bearer ${token}` },
    data: data || {},
  });
  const text = await res.text();
  await ctx.dispose();
  return { status: res.status(), body: safeParse(text) };
}

function safeParse(text) {
  try { return JSON.parse(text); } catch { return text; }
}

test.describe('Exam review workflow — happy path @flow', () => {
  test('teacher creates draft → submits → admin rejects → teacher resubmits → admin publishes', async () => {
    const teacher = await login('teacher');
    const admin = await login('admin');

    // 1. Teacher creates a draft.
    const created = await post(teacher.token, '/api/exams/groups', {
      name: 'E2E Review Flow ' + Date.now(),
      description: 'e2e generated',
      exam_type: 'test',
      duration_minutes: 30,
      total_marks: 50,
      passing_marks: 25,
    });
    expect(created.status).toBe(201);
    expect(created.body.status).toBe('draft');
    const groupId = created.body.id;

    // 2. Teacher submits.
    const submitted = await post(teacher.token, `/api/exams/groups/${groupId}/submit`);
    expect(submitted.status).toBe(200);
    expect(submitted.body.status).toBe('pending_review');

    // 3. Admin sees it in the queue.
    const queue = await get(admin.token, '/api/exams/groups/pending');
    expect(queue.status).toBe(200);
    expect(Array.isArray(queue.body)).toBe(true);
    const inQueue = queue.body.find(g => g.id === groupId);
    expect(inQueue).toBeTruthy();
    expect(inQueue.status).toBe('pending_review');

    // 4. Admin rejects with a reason.
    const rejected = await post(admin.token, `/api/exams/groups/${groupId}/reject`, {
      reason: 'Needs more questions',
    });
    expect(rejected.status).toBe(200);
    expect(rejected.body.status).toBe('rejected');
    expect(rejected.body.rejection_reason).toBe('Needs more questions');
    expect(rejected.body.reviewed_by).toBe(admin.user.id);
    expect(rejected.body.reviewed_at).toBeTruthy();

    // 5. After reject, the group should be GONE from the pending queue.
    const queueAfterReject = await get(admin.token, '/api/exams/groups/pending');
    expect(queueAfterReject.status).toBe(200);
    expect(queueAfterReject.body.find(g => g.id === groupId)).toBeFalsy();

    // 6. Teacher can resubmit a rejected group.
    const resubmitted = await post(teacher.token, `/api/exams/groups/${groupId}/submit`);
    expect(resubmitted.status).toBe(200);
    expect(resubmitted.body.status).toBe('pending_review');
    expect(resubmitted.body.rejection_reason).toBeNull();

    // 7. Admin creates a draft schedule.
    // (We don't strictly need a real class_id/subject_id here — the
    // schedules table doesn't enforce FK on those, just on exam_group_id
    // and the explicit values. We use the first class/subject from the
    // list endpoint to satisfy any schema constraints; if the demo seed
    // is empty, we just call it and assert the response.)
    const sched = await post(admin.token, '/api/exams/schedules', {
      exam_group_id: groupId,
      class_id: 1,
      subject_id: 1,
      start_time: '2027-01-01T09:00:00.000Z',
      end_time:   '2027-01-01T10:00:00.000Z',
      duration_minutes: 60,
    });
    expect(sched.status).toBe(201);
    expect(sched.body.is_active).toBe(0);
    const scheduleId = sched.body.id;

    // 8. Admin publishes the schedule.
    const published = await post(admin.token, `/api/exams/schedules/${scheduleId}/publish`);
    expect(published.status).toBe(200);
    expect(published.body.is_active).toBe(1);

    // 9. The parent group should now be approved.
    const finalGroup = await get(admin.token, `/api/exams/groups/${groupId}`);
    expect(finalGroup.status).toBe(200);
    expect(finalGroup.body.status).toBe('approved');
    expect(finalGroup.body.reviewed_by).toBe(admin.user.id);

    // 10. Republishing should be a no-op (409).
    const republish = await post(admin.token, `/api/exams/schedules/${scheduleId}/publish`);
    expect(republish.status).toBe(409);

    // Cleanup: delete the test data so re-runs don't pile up.
    await post(admin.token, `/api/exams/groups/${groupId}/reject`, { reason: 'e2e cleanup' })
      .catch(() => { /* ignore — group is already approved, will leave it */ });
  });
});

test.describe('Exam review workflow — error paths @errors', () => {
  test('reject without a reason returns 400', async () => {
    const admin = await login('admin');
    const res = await post(admin.token, '/api/exams/groups/1/reject', {});
    expect([400, 404]).toContain(res.status); // 400 if exists, 404 if id is gone
  });

  test('submit on an already-approved group returns 409', async () => {
    // Find an approved group via direct API. If none, skip.
    const admin = await login('admin');
    const list = await get(admin.token, '/api/exams/groups');
    if (list.status !== 200 || !Array.isArray(list.body)) {
      test.skip(true, 'could not list groups');
      return;
    }
    const approved = list.body.find(g => g.status === 'approved');
    if (!approved) {
      test.skip(true, 'no approved group available to test 409');
      return;
    }
    const res = await post(admin.token, `/api/exams/groups/${approved.id}/submit`);
    expect(res.status).toBe(409);
  });
});

test.describe('Exam review workflow — RBAC @rbac', () => {
  test('student is forbidden on every admin endpoint', async () => {
    const student = await login('student');
    const checks = [
      { method: 'get',  path: '/api/exams/groups/pending' },
      { method: 'post', path: '/api/exams/groups/1/reject', data: { reason: 'x' } },
      { method: 'post', path: '/api/exams/schedules/1/publish' },
    ];
    for (const c of checks) {
      const r = c.method === 'get'
        ? await get(student.token, c.path)
        : await post(student.token, c.path, c.data);
      expect(r.status, `${c.method.toUpperCase()} ${c.path} should be 403, got ${r.status}`).toBe(403);
    }
  });

  test('teacher is forbidden on admin-only endpoints (publish, reject, list-pending)', async () => {
    const teacher = await login('teacher');
    const checks = [
      { method: 'get',  path: '/api/exams/groups/pending' },
      { method: 'post', path: '/api/exams/groups/1/reject', data: { reason: 'x' } },
      { method: 'post', path: '/api/exams/schedules/1/publish' },
    ];
    for (const c of checks) {
      const r = c.method === 'get'
        ? await get(teacher.token, c.path)
        : await post(teacher.token, c.path, c.data);
      expect(r.status, `${c.method.toUpperCase()} ${c.path} should be 403, got ${r.status}`).toBe(403);
    }
  });

  test('anonymous (no token) gets 401 on the new endpoints', async () => {
    const checks = [
      { method: 'get',  path: '/api/exams/groups/pending' },
      { method: 'post', path: '/api/exams/groups/1/submit' },
      { method: 'post', path: '/api/exams/groups/1/reject', data: { reason: 'x' } },
      { method: 'post', path: '/api/exams/schedules/1/publish' },
    ];
    for (const c of checks) {
      const r = c.method === 'get'
        ? await get(null, c.path)
        : await post(null, c.path, c.data);
      expect(r.status, `${c.method.toUpperCase()} ${c.path} should be 401, got ${r.status}`).toBe(401);
    }
  });
});
