import { describe, expect, it } from 'vitest';

import { ForbiddenError, NotFoundError, UnauthenticatedError } from '../errors/index.js';
import {
  testCashier,
  testDean,
  testFaculty,
  testRegistrar,
  testStudent,
  testSuperAdmin,
} from '../testing/context.js';
import { anonymousContext } from './context.js';
import { authorize, can, hasPermission, isInScope, scopeFilterFor } from './authorize.js';
import { ALL_PERMISSIONS, DEFAULT_ROLE_PERMISSIONS, PERMISSION_CATALOGUE } from './permissions.js';
import { evaluateStudentAccess, resolveStudentAccess, studentAudienceFor } from './student-access.js';

describe('permission catalogue', () => {
  it('has no duplicate keys', () => {
    const keys = PERMISSION_CATALOGUE.map((entry) => entry.key);
    expect(new Set(keys).size).toBe(keys.length);
  });

  it('gives every permission a description and a group', () => {
    for (const entry of PERMISSION_CATALOGUE) {
      expect(entry.description.length).toBeGreaterThan(0);
      expect(entry.group.length).toBeGreaterThan(0);
    }
  });

  it('only seeds roles with permissions that exist in the catalogue', () => {
    const known = new Set<string>(ALL_PERMISSIONS);
    for (const [role, permissions] of Object.entries(DEFAULT_ROLE_PERMISSIONS)) {
      for (const permission of permissions) {
        expect(known.has(permission), `${role} references unknown permission ${permission}`).toBe(
          true,
        );
      }
    }
  });
});

describe('authorize — authentication', () => {
  it('rejects an anonymous caller before checking anything else', () => {
    const ctx = anonymousContext('institution-test');
    expect(() => authorize(ctx, 'student.read')).toThrow(UnauthenticatedError);
  });
});

describe('authorize — permissions', () => {
  it('allows an actor holding the permission', () => {
    expect(() => authorize(testRegistrar(), 'student.read')).not.toThrow();
  });

  it('rejects an actor missing the permission', () => {
    expect(() => authorize(testCashier(), 'grade.publish')).toThrow(ForbiddenError);
  });

  it('reports which permission was required, without leaking anything else', () => {
    try {
      authorize(testCashier(), 'grade.encode');
      expect.unreachable('should have thrown');
    } catch (error) {
      expect(error).toBeInstanceOf(ForbiddenError);
      expect((error as ForbiddenError).toPublicJSON().meta).toEqual({
        requiredPermission: 'grade.encode',
      });
    }
  });

  it('grants a superadmin every permission without listing them', () => {
    const superAdmin = testSuperAdmin();
    expect(superAdmin.permissions.size).toBe(0);
    for (const permission of ALL_PERMISSIONS) {
      expect(hasPermission(superAdmin, permission)).toBe(true);
    }
  });
});

describe('authorize — resource scope', () => {
  it('allows a Dean to act within an assigned college', () => {
    const dean = testDean(['college-medicine']);
    expect(() => authorize(dean, 'application.review', { collegeId: 'college-medicine' })).not.toThrow();
  });

  it('masks an out-of-scope college as not found, so existence cannot be probed', () => {
    const dean = testDean(['college-medicine']);
    expect(() => authorize(dean, 'application.review', { collegeId: 'college-nursing' })).toThrow(
      NotFoundError,
    );
  });

  it('can be asked to report a scope failure as forbidden instead', () => {
    const dean = testDean(['college-medicine']);
    expect(() =>
      authorize(
        dean,
        'application.review',
        { collegeId: 'college-nursing' },
        { maskAsNotFound: false },
      ),
    ).toThrow(ForbiddenError);
  });

  it('confines a student to their own record', () => {
    const student = testStudent('student-1');
    expect(isInScope(student, { studentId: 'student-1' })).toBe(true);
    expect(isInScope(student, { studentId: 'student-2' })).toBe(false);
  });

  it('confines faculty to their assigned sections', () => {
    const faculty = testFaculty(['section-a', 'section-b']);
    expect(isInScope(faculty, { sectionOfferingId: 'section-a' })).toBe(true);
    expect(isInScope(faculty, { sectionOfferingId: 'section-z' })).toBe(false);
  });

  it('does not narrow institution-wide roles that hold no assignment', () => {
    const registrar = testRegistrar();
    expect(isInScope(registrar, { collegeId: 'college-anything' })).toBe(true);
  });

  it('lets a superadmin through any scope', () => {
    expect(isInScope(testSuperAdmin(), { collegeId: 'x', studentId: 'y' })).toBe(true);
  });
});

describe('scopeFilterFor', () => {
  it('returns null dimensions for an unrestricted actor', () => {
    expect(scopeFilterFor(testRegistrar())).toEqual({
      collegeIds: null,
      programIds: null,
      sectionOfferingIds: null,
      studentId: null,
    });
  });

  it('returns the college list for a Dean', () => {
    expect(scopeFilterFor(testDean(['c1', 'c2'])).collegeIds).toEqual(['c1', 'c2']);
  });

  it('returns the student id for a student', () => {
    expect(scopeFilterFor(testStudent('student-9')).studentId).toBe('student-9');
  });
});

describe('student access resolution', () => {
  it('confines a student to themselves', () => {
    const mode = resolveStudentAccess(testStudent('student-1'));
    expect(mode).toEqual({ kind: 'SELF', studentId: 'student-1' });
    expect(evaluateStudentAccess(mode, 'student-1')).toBe('ALLOW');
    expect(evaluateStudentAccess(mode, 'student-2')).toBe('DENY');
  });

  it('requires a section lookup for faculty rather than allowing by default', () => {
    const mode = resolveStudentAccess(testFaculty(['section-a']));
    expect(mode.kind).toBe('SECTION');
    expect(evaluateStudentAccess(mode, 'any-student')).toBe('REQUIRES_LOOKUP');
  });

  it('denies a faculty member with no assigned sections', () => {
    const mode = resolveStudentAccess(testFaculty([]));
    expect(evaluateStudentAccess(mode, 'any-student')).toBe('DENY');
  });

  it('scopes a Dean to their colleges', () => {
    const mode = resolveStudentAccess(testDean(['college-medicine']));
    expect(mode).toEqual({ kind: 'COLLEGE', collegeIds: ['college-medicine'] });
  });

  it('denies an actor without student.read', () => {
    const mode = resolveStudentAccess(
      testFaculty(['section-a'], { permissions: ['grade.encode'] }),
    );
    expect(mode).toEqual({ kind: 'NONE' });
    expect(evaluateStudentAccess(mode, 'student-1')).toBe('DENY');
  });

  it('gives institution-wide roles full visibility', () => {
    expect(resolveStudentAccess(testRegistrar())).toEqual({ kind: 'ALL' });
  });
});

describe('student field projection', () => {
  it('gives faculty a narrow projection with no financial fields', () => {
    const audience = studentAudienceFor(testFaculty(['section-a']));
    expect(audience).toBe('faculty');
    const fields = ['id', 'studentNumber', 'lastName', 'firstName', 'middleName', 'yearLevel', 'programCode', 'status'];
    expect(fields).not.toContain('balance');
  });

  it('gives the registrar the full record', () => {
    expect(studentAudienceFor(testRegistrar())).toBe('registrar');
  });

  it('gives a student their own full record', () => {
    expect(studentAudienceFor(testStudent('s1'))).toBe('self');
  });

  it('gives a cashier the financial audience', () => {
    expect(studentAudienceFor(testCashier())).toBe('cashier');
  });
});

describe('can — the UI convenience check', () => {
  it('mirrors authorize without throwing', () => {
    const dean = testDean(['college-medicine']);
    expect(can(dean, 'application.review', { collegeId: 'college-medicine' })).toBe(true);
    expect(can(dean, 'application.review', { collegeId: 'college-nursing' })).toBe(false);
    expect(can(dean, 'payment.create')).toBe(false);
  });

  it('is false for anonymous callers', () => {
    expect(can(anonymousContext('institution-test'), 'student.read')).toBe(false);
  });
});

describe('privacy invariants (SPEC.md §36 Scenario 6)', () => {
  it('denies a cashier any grade mutation permission', () => {
    const cashier = testCashier();
    for (const permission of ['grade.encode', 'grade.submit', 'grade.publish', 'grade.change_approve'] as const) {
      expect(hasPermission(cashier, permission)).toBe(false);
    }
  });

  it('denies a student every staff read permission', () => {
    const student = testStudent('student-1');
    for (const permission of ['student.read', 'application.read', 'payment.read', 'audit.read'] as const) {
      expect(hasPermission(student, permission)).toBe(false);
    }
  });

  it('keeps the seeded cashier role free of academic mutation permissions', () => {
    const cashierPermissions: readonly string[] = DEFAULT_ROLE_PERMISSIONS.CASHIER;
    for (const permission of cashierPermissions) {
      expect(permission.startsWith('grade.')).toBe(false);
      expect(permission.startsWith('curriculum.')).toBe(false);
    }
  });

  it('keeps the seeded faculty role free of financial permissions', () => {
    const facultyPermissions: readonly string[] = DEFAULT_ROLE_PERMISSIONS.FACULTY;
    for (const permission of facultyPermissions) {
      expect(permission.startsWith('payment.')).toBe(false);
      expect(permission.startsWith('assessment.')).toBe(false);
    }
  });
});
