// Unit tests for requireRole middleware.
//
// These exercise the factory + shorthand helpers without standing up an
// Express app. We keep this Vitest-friendly (no jest globals) — the
// project uses Vitest, not Jest.

const {
  ROLE,
  requireRole,
  requireAdmin,
  requireStaff,
  requireOwnerOrAdmin,
  ADMIN_ROLES,
  ADMIN_AND_PRINCIPAL,
  STAFF_ROLES,
} = require('../src/middleware/requireRole');

function fakeRes() {
  const r = { statusCode: null, body: null };
  r.status = (code) => {
    r.statusCode = code;
    return r;
  };
  r.json = (body) => {
    r.body = body;
    return r;
  };
  return r;
}

function captureNext() {
  const calls = [];
  const fn = (...args) => {
    calls.push(args);
  };
  return [fn, calls];
}

describe('requireRole middleware factory', () => {
  it('allows a caller whose role is in the allowed set', () => {
    const mw = requireRole(ROLE.TEACHER, ROLE.SCHOOL_ADMIN);
    const [next, calls] = captureNext();
    mw({ user: { role: ROLE.TEACHER } }, fakeRes(), next);
    expect(calls.length).toBe(1);
  });

  it('rejects with 403 when the caller role is not allowed', () => {
    const mw = requireRole(ROLE.SCHOOL_ADMIN);
    const res = fakeRes();
    const [next, calls] = captureNext();
    mw({ user: { role: ROLE.STUDENT } }, res, next);
    expect(res.statusCode).toBe(403);
    expect(res.body.error).toMatch(/forbidden/i);
    expect(calls.length).toBe(0);
  });

  it('rejects with 401 when no user is attached (defence in depth)', () => {
    const mw = requireRole(ROLE.TEACHER);
    const res = fakeRes();
    const [next, calls] = captureNext();
    mw({}, res, next);
    expect(res.statusCode).toBe(401);
    expect(calls.length).toBe(0);
  });

  it('treats missing role as unauthenticated', () => {
    const mw = requireRole(ROLE.TEACHER);
    const res = fakeRes();
    const [next, calls] = captureNext();
    mw({ user: {} }, res, next);
    expect(res.statusCode).toBe(401);
    expect(calls.length).toBe(0);
  });
});

describe('requireAdmin shorthand', () => {
  it('allows school_admin, systems_admin, and principal', () => {
    for (const role of ADMIN_AND_PRINCIPAL) {
      const [next, calls] = captureNext();
      requireAdmin({ user: { role } }, fakeRes(), next);
      expect(calls.length).toBe(1);
    }
  });

  it('rejects teachers, students, and parents', () => {
    for (const role of [ROLE.TEACHER, ROLE.STUDENT, ROLE.PARENT, ROLE.BURSAR]) {
      const res = fakeRes();
      const [next, calls] = captureNext();
      requireAdmin({ user: { role } }, res, next);
      expect(res.statusCode).toBe(403);
      expect(calls.length).toBe(0);
    }
  });
});

describe('requireStaff shorthand', () => {
  it('admits every staff role (admin, hr, bursar, librarian, etc.)', () => {
    for (const role of STAFF_ROLES) {
      const [next, calls] = captureNext();
      requireStaff({ user: { role } }, fakeRes(), next);
      expect(calls.length).toBe(1);
    }
  });

  it('admits teachers and librarian + clubs_head (operational roles)', () => {
    for (const role of [ROLE.TEACHER, ROLE.LIBRARIAN, ROLE.CLUBS_HEAD]) {
      const [next, calls] = captureNext();
      requireStaff({ user: { role } }, fakeRes(), next);
      expect(calls.length).toBe(1);
    }
  });

  it('rejects students and parents', () => {
    for (const role of [ROLE.STUDENT, ROLE.PARENT]) {
      const res = fakeRes();
      const [next, calls] = captureNext();
      requireStaff({ user: { role } }, res, next);
      expect(res.statusCode).toBe(403);
      expect(calls.length).toBe(0);
    }
  });
});

describe('requireOwnerOrAdmin', () => {
  it('allows admins unconditionally', () => {
    for (const role of ADMIN_ROLES) {
      const [next, calls] = captureNext();
      requireOwnerOrAdmin({ user: { role, id: 1 }, params: {} }, fakeRes(), next);
      expect(calls.length).toBe(1);
    }
  });

  it('allows the user whose id matches the URL param', () => {
    const [next, calls] = captureNext();
    requireOwnerOrAdmin(
      { user: { role: ROLE.STUDENT, id: 42 }, params: { id: '42' } },
      fakeRes(),
      next
    );
    expect(calls.length).toBe(1);
  });

  it('rejects a non-owner who is not admin and not parent', () => {
    const res = fakeRes();
    const [next, calls] = captureNext();
    requireOwnerOrAdmin(
      { user: { role: ROLE.STUDENT, id: 1 }, params: { id: '2' } },
      res,
      next
    );
    expect(res.statusCode).toBe(403);
    expect(calls.length).toBe(0);
  });

  it('allows parents through (controller does its own scope check)', () => {
    const [next, calls] = captureNext();
    requireOwnerOrAdmin(
      { user: { role: ROLE.PARENT, id: 7 }, params: { studentId: '999' } },
      fakeRes(),
      next
    );
    expect(calls.length).toBe(1);
  });
});

describe('ROLE constant', () => {
  it('exposes the role strings in a stable shape', () => {
    expect(ROLE.SCHOOL_ADMIN).toBe('school_admin');
    expect(ROLE.SYSTEMS_ADMIN).toBe('systems_admin');
  });

  it('is frozen so callers cannot mutate the registry', () => {
    expect(Object.isFrozen(ROLE)).toBe(true);
  });
});
