import { afterEach, describe, mock, test } from 'node:test';
import assert from 'node:assert/strict';
import db from '@/db/models';
import DirectMessagesService from '@/services/direct_messages';
import { createTestUser } from '@/test-utils';
import { ROLE_NAMES } from '@/shared/constants/roles';
import { FEATURE_PERMISSIONS } from '@/shared/constants/product-permissions';
import ForbiddenError from '@/shared/errors/forbidden';
import type { CurrentUser } from '@/db/api/types';

interface UserRow {
  id: string;
  firstName?: string | null;
  lastName?: string | null;
  email?: string | null;
  classId?: string | null;
  campusId?: string | null;
  app_role?: { name: string };
}

interface GuardianStudentRow {
  guardianId: string;
  studentId: string;
}

interface ClassRow {
  id: string;
  campusId: string | null;
}

interface DirectMessageRow {
  id: string;
  senderId: string;
  recipientId: string;
  studentId: string | null;
  body: string;
  readAt: Date | null;
  createdAt: Date;
}

type UsersFindAllResult = Awaited<ReturnType<typeof db.users.findAll>>;
type GuardianStudentsFindAllResult = Awaited<ReturnType<typeof db.guardian_students.findAll>>;
type ClassesFindAllResult = Awaited<ReturnType<typeof db.classes.findAll>>;
type DirectMessagesFindAllResult = Awaited<ReturnType<typeof db.direct_messages.findAll>>;
type DirectMessagesCreateResult = Awaited<ReturnType<typeof db.direct_messages.create>>;

function users(rows: readonly UserRow[]): UsersFindAllResult {
  return rows as unknown as UsersFindAllResult;
}

function guardianLinks(rows: readonly GuardianStudentRow[]): GuardianStudentsFindAllResult {
  return rows as unknown as GuardianStudentsFindAllResult;
}

function classes(rows: readonly ClassRow[]): ClassesFindAllResult {
  return rows as unknown as ClassesFindAllResult;
}

function messages(rows: readonly DirectMessageRow[]): DirectMessagesFindAllResult {
  return rows as unknown as DirectMessagesFindAllResult;
}

function createdMessage(row: DirectMessageRow): DirectMessagesCreateResult {
  return row as unknown as DirectMessagesCreateResult;
}

function teacher(overrides: Partial<CurrentUser> = {}): CurrentUser {
  return createTestUser({
    id: 'teacher-1',
    organizationId: 'org-1',
    organizations: { id: 'org-1' },
    classId: 'class-1',
    app_role: {
      name: ROLE_NAMES.TEACHER,
      globalAccess: false,
      permissions: [{ name: FEATURE_PERMISSIONS.READ_PARENT_COMM }],
    },
    ...overrides,
  });
}

function guardian(overrides: Partial<CurrentUser> = {}): CurrentUser {
  return createTestUser({
    id: 'guardian-1',
    organizationId: 'org-1',
    organizations: { id: 'org-1' },
    app_role: {
      name: ROLE_NAMES.GUARDIAN,
      globalAccess: false,
      permissions: [{ name: FEATURE_PERMISSIONS.READ_PARENT_COMM }],
    },
    ...overrides,
  });
}

afterEach(() => {
  mock.restoreAll();
});

describe('DirectMessagesService.contacts', () => {
  test('does not let global access imply parent communication access', async () => {
    await assert.rejects(
      () => DirectMessagesService.contacts(createTestUser({
        app_role: { name: ROLE_NAMES.SUPER_ADMIN, globalAccess: true },
      })),
      ForbiddenError,
    );
  });

  test('teacher sees guardian contacts separated by student', async () => {
    let usersFindAllCall = 0;
    const usersFindAll = (async () => {
      usersFindAllCall += 1;
      if (usersFindAllCall === 1) {
        return users([
          { id: 'student-1', firstName: 'Amy', lastName: 'Adams', classId: 'class-1', campusId: 'campus-1' },
          { id: 'student-2', firstName: 'Ben', lastName: 'Brown', classId: 'class-1', campusId: 'campus-1' },
        ]);
      }
      return users([
        { id: 'guardian-1', firstName: 'Pat', lastName: 'Adams' },
        { id: 'guardian-2', firstName: 'Robin', lastName: 'Brown' },
      ]);
    }) as typeof db.users.findAll;

    mock.method(db.users, 'findAll', usersFindAll);
    mock.method(db.guardian_students, 'findAll', (async () => guardianLinks([
      { guardianId: 'guardian-1', studentId: 'student-1' },
      { guardianId: 'guardian-2', studentId: 'student-2' },
    ])) as typeof db.guardian_students.findAll);

    const result = await DirectMessagesService.contacts(teacher());

    assert.deepEqual(
      result.rows.map((row) => ({
        key: row.conversationKey,
        userId: row.userId,
        studentId: row.studentId,
        studentName: row.studentName,
      })),
      [
        { key: 'guardian-1:student-1', userId: 'guardian-1', studentId: 'student-1', studentName: 'Amy Adams' },
        { key: 'guardian-2:student-2', userId: 'guardian-2', studentId: 'student-2', studentName: 'Ben Brown' },
      ],
    );
  });

  test('guardian sees teacher and office manager through linked student', async () => {
    let usersFindAllCall = 0;
    const usersFindAll = (async () => {
      usersFindAllCall += 1;
      if (usersFindAllCall === 1) {
        return users([
          { id: 'student-1', firstName: 'Amy', lastName: 'Adams', classId: 'class-1', campusId: null },
        ]);
      }
      if (usersFindAllCall === 2) {
        return users([
          { id: 'teacher-1', firstName: 'Tina', lastName: 'Teacher', classId: 'class-1', app_role: { name: ROLE_NAMES.TEACHER } },
        ]);
      }
      return users([
        { id: 'office-1', firstName: 'Oscar', lastName: 'Office', campusId: 'campus-1', app_role: { name: ROLE_NAMES.OFFICE_MANAGER } },
      ]);
    }) as typeof db.users.findAll;

    mock.method(db.guardian_students, 'findAll', (async () => guardianLinks([
      { guardianId: 'guardian-1', studentId: 'student-1' },
    ])) as typeof db.guardian_students.findAll);
    mock.method(db.classes, 'findAll', (async () => classes([
      { id: 'class-1', campusId: 'campus-1' },
    ])) as typeof db.classes.findAll);
    mock.method(db.users, 'findAll', usersFindAll);

    const result = await DirectMessagesService.contacts(guardian());

    assert.deepEqual(
      result.rows.map((row) => ({
        key: row.conversationKey,
        userId: row.userId,
        role: row.role,
        studentId: row.studentId,
      })),
      [
        { key: 'teacher-1:student-1', userId: 'teacher-1', role: ROLE_NAMES.TEACHER, studentId: 'student-1' },
        { key: 'office-1:student-1', userId: 'office-1', role: ROLE_NAMES.OFFICE_MANAGER, studentId: 'student-1' },
      ],
    );
  });
});

describe('DirectMessagesService.conversations', () => {
  test('keeps same counterpart separated by student', async () => {
    const firstAt = new Date('2026-06-14T10:00:00.000Z');
    const secondAt = new Date('2026-06-14T11:00:00.000Z');

    mock.method(db.direct_messages, 'findAll', (async () => messages([
      {
        id: 'm-2',
        senderId: 'guardian-1',
        recipientId: 'teacher-1',
        studentId: 'student-2',
        body: 'About Ben',
        readAt: null,
        createdAt: secondAt,
      },
      {
        id: 'm-1',
        senderId: 'guardian-1',
        recipientId: 'teacher-1',
        studentId: 'student-1',
        body: 'About Amy',
        readAt: null,
        createdAt: firstAt,
      },
    ])) as typeof db.direct_messages.findAll);

    let usersFindAllCall = 0;
    const usersFindAll = (async () => {
      usersFindAllCall += 1;
      if (usersFindAllCall === 1) {
        return users([{ id: 'guardian-1', firstName: 'Pat', lastName: 'Adams' }]);
      }
      return users([
        { id: 'student-1', firstName: 'Amy', lastName: 'Adams' },
        { id: 'student-2', firstName: 'Ben', lastName: 'Brown' },
      ]);
    }) as typeof db.users.findAll;
    mock.method(db.users, 'findAll', usersFindAll);

    const result = await DirectMessagesService.conversations(teacher());

    assert.deepEqual(
      result.rows.map((row) => ({
        key: row.conversationKey,
        studentId: row.studentId,
        studentName: row.studentName,
        unread: row.unread,
      })),
      [
        { key: 'guardian-1:student-2', studentId: 'student-2', studentName: 'Ben Brown', unread: 1 },
        { key: 'guardian-1:student-1', studentId: 'student-1', studentName: 'Amy Adams', unread: 1 },
      ],
    );
  });
});

describe('DirectMessagesService thread/send access', () => {
  function mockTeacherContactsForTwoStudents(): void {
    let usersFindAllCall = 0;
    const usersFindAll = (async () => {
      usersFindAllCall += 1;
      if (usersFindAllCall === 1) {
        return users([
          { id: 'student-1', firstName: 'Amy', lastName: 'Adams', classId: 'class-1' },
          { id: 'student-2', firstName: 'Ben', lastName: 'Brown', classId: 'class-1' },
        ]);
      }
      return users([{ id: 'guardian-1', firstName: 'Pat', lastName: 'Adams' }]);
    }) as typeof db.users.findAll;

    mock.method(db.users, 'findAll', usersFindAll);
    mock.method(db.guardian_students, 'findAll', (async () => guardianLinks([
      { guardianId: 'guardian-1', studentId: 'student-1' },
      { guardianId: 'guardian-1', studentId: 'student-2' },
    ])) as typeof db.guardian_students.findAll);
  }

  test('thread rejects ambiguous same-counterpart access without studentId', async () => {
    mockTeacherContactsForTwoStudents();

    await assert.rejects(
      () => DirectMessagesService.thread('guardian-1', undefined, teacher()),
      ForbiddenError,
    );
  });

  test('send stores the selected student context', async () => {
    mockTeacherContactsForTwoStudents();
    const commits: string[] = [];
    const createdRows: unknown[] = [];

    mock.method(db.sequelize, 'transaction', (async () => ({
      commit: async () => { commits.push('commit'); },
      rollback: async () => { commits.push('rollback'); },
    })) as typeof db.sequelize.transaction);

    mock.method(db.direct_messages, 'create', (async (data: unknown) => {
      createdRows.push(data);
      return createdMessage({
        id: 'message-1',
        senderId: 'teacher-1',
        recipientId: 'guardian-1',
        studentId: 'student-2',
        body: 'Hello',
        readAt: null,
        createdAt: new Date('2026-06-14T12:00:00.000Z'),
      });
    }) as typeof db.direct_messages.create);

    const result = await DirectMessagesService.send(
      { recipientId: 'guardian-1', studentId: 'student-2', body: 'Hello' },
      teacher(),
    );

    assert.equal(result.id, 'message-1');
    assert.equal(commits.length, 1);
    assert.deepEqual(createdRows, [{
      senderId: 'teacher-1',
      recipientId: 'guardian-1',
      studentId: 'student-2',
      body: 'Hello',
      organizationId: 'org-1',
      createdById: 'teacher-1',
      updatedById: 'teacher-1',
    }]);
  });
});
