/**
 * E2E tests for the bulk marks CSV import endpoint.
 *
 * Target:   POST /api/marks/bulk
 * Caller:   teacher demo account (`teacher@school.com / teacher123`)
 *           is the production happy path, but for setup convenience these
 *           tests use the admin account so we can also create the
 *           dependent course + assignment rows through the API. The
 *           marks endpoint allows admin and teacher equally.
 * Header:   assignment_id,student_id,score,feedback
 *
 * Three specs:
 *   1. Happy path — 30 valid rows produce 30 inserted (0 errored).
 *   2. One invalid row — 30 rows, one references a missing student;
 *      29 inserted, 1 errored.
 *   3. Non-CSV file — a PNG is rejected by the mime allowlist with 400.
 *
 * Test data setup: a fresh course + assignment is created per run;
 * student IDs come from GET /api/users?role=student. The
 * marks.controller validates both columns with explicit header + per-row
 * error reporting (added in ops/sqlite-backup 2026-07-18).
 *
 * Pre-reqs (same as fixes.spec.ts):
 *   - server on http://localhost:3001 with seeded subjects + students
 *   - client on http://localhost:3000 (not strictly needed for these
 *     specs since we go through the API directly, but playwright.config
 *     uses it for the baseURL fallback)
 */
import { test, expect, request as pwRequest } from '@playwright/test';

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

const TEACHER = { email: 'teacher@school.com', password: 'teacher123' };
const ADMIN   = { email: 'admin@school.com',   password: 'admin123'   };

/** Login and return a request context bound to the API with the JWT in the
 *  default Authorization header. */
async function loginAs(creds: { email: string; password: string }) {
  const ctx = await pwRequest.newContext({ baseURL: API });
  const res = await ctx.post('/api/auth/login', { data: creds });
  expect(res.status(), `login as ${creds.email}`).toBe(200);
  const body = await res.json();
  const token = body.token as string;
  // Re-issue a context that always sends the bearer token.
  const authed = await pwRequest.newContext({
    baseURL: API,
    extraHTTPHeaders: { Authorization: `Bearer ${token}` },
  });
  await ctx.dispose();
  return { ctx: authed, token, user: body.user };
}

/** Pick an existing course_id from GET /api/courses. The assignments
 *  controller only validates that course_id references a row in subjects
 *  (the FK), so any seeded course is fine. */
async function pickCourseId(ctx: any): Promise<number> {
  const res = await ctx.get('/api/courses');
  expect(res.status(), 'GET /api/courses').toBe(200);
  const arr = await res.json();
  expect(Array.isArray(arr)).toBe(true);
  expect(arr.length).toBeGreaterThan(0);
  return arr[0].id as number;
}

/** Pick the first N distinct student IDs. Returns numeric IDs that
 *  the marks controller will accept. */
async function pickStudentIds(ctx: any, n: number): Promise<number[]> {
  const res = await ctx.get('/api/users', { params: { role: 'student', limit: String(n) } });
  expect(res.status(), 'GET /api/users?role=student').toBe(200);
  const body = await res.json();
  const users = body.users || body;
  expect(Array.isArray(users)).toBe(true);
  expect(users.length).toBeGreaterThanOrEqual(n);
  return users.slice(0, n).map((u: any) => u.id as number);
}

/** Create a fresh assignment so the upload counts as inserts (not updates). */
async function createAssignment(ctx: any, courseId: number): Promise<number> {
  const res = await ctx.post('/api/assignments', {
    data: {
      course_id: courseId,
      title: `E2E bulk-marks ${new Date().toISOString()}`,
      description: 'Created by client/e2e/bulk-marks.spec.ts',
      max_score: 100,
      submission_type: 'file',
    },
  });
  expect(res.status(), 'POST /api/assignments').toBe(201);
  const body = await res.json();
  expect(typeof body.id).toBe('number');
  return body.id as number;
}

function buildCsv(rows: Array<{ assignment_id: number; student_id: number; score: number; feedback: string }>): Buffer {
  const lines = ['assignment_id,student_id,score,feedback'];
  for (const r of rows) {
    lines.push(`${r.assignment_id},${r.student_id},${r.score},"${r.feedback.replace(/"/g, '""')}"`);
  }
  return Buffer.from(lines.join('\n'), 'utf-8');
}

/** Build the per-test setup the three specs share: admin login, fresh
 *  assignment against an existing course, and a list of student IDs. */
async function setupTestData() {
  const { ctx } = await loginAs(ADMIN);
  const courseId     = await pickCourseId(ctx);
  const assignmentId = await createAssignment(ctx, courseId);
  const studentIds   = await pickStudentIds(ctx, 30);
  return { ctx, assignmentId, studentIds };
}

test.describe('bulk marks — POST /api/marks/bulk', () => {
  test('1. happy path: 30 valid rows → 30 inserted', async () => {
    const { ctx, assignmentId, studentIds } = await setupTestData();

    const csv = buildCsv(studentIds.map((sid, i) => ({
      assignment_id: assignmentId,
      student_id:    sid,
      score:         50 + (i % 51),     // 50..100
      feedback:      `bulk-marks spec 1 row ${i + 1}`,
    })));

    const res = await ctx.post('/api/marks/bulk', {
      multipart: {
        file: {
          name:     'marks.csv',
          mimeType: 'text/csv',
          buffer:   csv,
        },
      },
    });
    expect(res.status(), 'bulk upload status').toBe(200);
    const body = await res.json();
    expect(body.total_rows).toBe(30);
    expect(body.inserted).toBe(30);
    expect(body.updated).toBe(0);
    expect(Array.isArray(body.errors)).toBe(true);
    expect(body.errors.length).toBe(0);
    await ctx.dispose();
  });

  test('2. one invalid row: 30 rows → 29 inserted, 1 errored', async () => {
    const { ctx, assignmentId, studentIds } = await setupTestData();

    // Replace student_id for one row with a non-existent user id.
    // The controller validates "student exists and role=student" before
    // counting it as an insert; this row should land in errors[].
    const BAD_STUDENT_ID = 9_999_999;
    const rows = studentIds.map((sid, i) => ({
      assignment_id: assignmentId,
      student_id:    i === 7 ? BAD_STUDENT_ID : sid,
      score:         60,
      feedback:      i === 7 ? 'this row references a missing student' : `spec 2 row ${i + 1}`,
    }));

    const csv = buildCsv(rows);

    const res = await ctx.post('/api/marks/bulk', {
      multipart: {
        file: {
          name:     'marks-with-one-bad-row.csv',
          mimeType: 'text/csv',
          buffer:   csv,
        },
      },
    });
    expect(res.status(), 'bulk upload status').toBe(200);
    const body = await res.json();
    expect(body.total_rows).toBe(30);
    expect(body.inserted).toBe(29);
    expect(body.updated).toBe(0);
    expect(Array.isArray(body.errors)).toBe(true);
    expect(body.errors.length).toBe(1);
    // The reported row number should be 9 (header is row 1, bad row is index 8).
    expect(body.errors[0].row).toBe(9);
    expect(String(body.errors[0].reason)).toMatch(/student_id 9999999 not found/i);
    await ctx.dispose();
  });

  test('3. non-CSV file → 400', async () => {
    const { ctx } = await loginAs(TEACHER);
    // A minimal 1x1 PNG. mime type is image/png — outside the controller's
    // allowlist of text/csv, application/csv, application/vnd.ms-excel,
    // text/plain, application/octet-stream, ''.
    const png = Buffer.from(
      '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489' +
      '0000000d49444154789c636000010000050001a5f645400000000049454e44ae426082',
      'hex'
    );

    const res = await ctx.post('/api/marks/bulk', {
      multipart: {
        file: {
          name:     'not-a-csv.png',
          mimeType: 'image/png',
          buffer:   png,
        },
      },
    });
    expect(res.status(), 'non-CSV upload status').toBe(400);
    const body = await res.json();
    expect(String(body.error || '')).toMatch(/unsupported mime type/i);
    await ctx.dispose();
  });
});