import { AttendanceSourceEnum } from '../enum/attendance-source.enum';
import { AttendanceStatus } from '../enum/attendance-status.enum';
import { AttendanceEvent } from '../interfaces/attendance-event.interface';
import {
  resolveAttendanceStatus,
  activeEvents,
  activeMarkFrom,
  deriveManualSource,
  resolveManualActor,
  isAttendedFromStatus,
} from './attendance-resolution.util';
import { ROLE_VALUES } from '../constants/strings-constants';

const S = AttendanceSourceEnum;
const St = AttendanceStatus;

let clock = 0;
/** Builds an event with a strictly increasing occurredAt so append order == time. */
function ev(
  source: AttendanceSourceEnum,
  status?: AttendanceStatus,
  performedBy: number | null = null,
): AttendanceEvent {
  clock += 1;
  const occurredAt = new Date(Date.UTC(2026, 0, 1, 0, 0, clock)).toISOString();
  return { source, status, occurredAt, performedBy };
}

beforeEach(() => {
  clock = 0;
});

describe('resolveAttendanceStatus — precedence truth table (PRD BR-SATT-002)', () => {
  it('empty log → UNKNOWN', () => {
    expect(resolveAttendanceStatus([])).toEqual({ status: St.UNKNOWN, decidedBySource: null });
    expect(resolveAttendanceStatus(undefined)).toEqual({ status: St.UNKNOWN, decidedBySource: null });
  });

  it('only join-click present → PRESENT', () => {
    const r = resolveAttendanceStatus([ev(S.JOIN_CLICK, St.PRESENT)]);
    expect(r).toEqual({ status: St.PRESENT, decidedBySource: S.JOIN_CLICK });
  });

  it('Zoom present outranks join-click present', () => {
    const r = resolveAttendanceStatus([ev(S.JOIN_CLICK, St.PRESENT), ev(S.ZOOM_WEBHOOK, St.PRESENT)]);
    expect(r.decidedBySource).toBe(S.ZOOM_WEBHOOK);
    expect(r.status).toBe(St.PRESENT);
  });

  it('Zoom absent beats join-click present (direction irrelevant, rank wins)', () => {
    const r = resolveAttendanceStatus([ev(S.JOIN_CLICK, St.PRESENT), ev(S.ZOOM_WEBHOOK, St.ABSENT)]);
    expect(r).toEqual({ status: St.ABSENT, decidedBySource: S.ZOOM_WEBHOOK });
  });

  it('RM absent beats all automated present', () => {
    const r = resolveAttendanceStatus([
      ev(S.JOIN_CLICK, St.PRESENT),
      ev(S.ZOOM_WEBHOOK, St.PRESENT),
      ev(S.QR_SCAN, St.PRESENT),
      ev(S.MANUAL_RM, St.ABSENT, 42),
    ]);
    expect(r).toEqual({ status: St.ABSENT, decidedBySource: S.MANUAL_RM });
  });

  it('coordinator present beats RM absent + Zoom absent', () => {
    const r = resolveAttendanceStatus([
      ev(S.ZOOM_WEBHOOK, St.ABSENT),
      ev(S.MANUAL_RM, St.ABSENT, 42),
      ev(S.MANUAL_COORDINATOR, St.PRESENT, 7),
    ]);
    expect(r).toEqual({ status: St.PRESENT, decidedBySource: S.MANUAL_COORDINATOR });
  });

  it('coordinator absent beats everything present', () => {
    const r = resolveAttendanceStatus([
      ev(S.JOIN_CLICK, St.PRESENT),
      ev(S.ZOOM_WEBHOOK, St.PRESENT),
      ev(S.MANUAL_RM, St.PRESENT, 42),
      ev(S.MANUAL_COORDINATOR, St.ABSENT, 7),
    ]);
    expect(r).toEqual({ status: St.ABSENT, decidedBySource: S.MANUAL_COORDINATOR });
  });
});

describe('resolveAttendanceStatus — legacy back-compat (TRD §5)', () => {
  it('event without status reads as PRESENT', () => {
    const legacy: AttendanceEvent = { source: S.MANUAL_ADMIN, occurredAt: '2020-01-01T00:00:00.000Z', performedBy: 1 };
    expect(resolveAttendanceStatus([legacy])).toEqual({ status: St.PRESENT, decidedBySource: S.MANUAL_ADMIN });
  });

  it('legacy MANUAL_ADMIN resolves at coordinator rank (dec-11)', () => {
    const r = resolveAttendanceStatus([ev(S.MANUAL_RM, St.ABSENT, 42), ev(S.MANUAL_ADMIN, St.PRESENT, 1)]);
    expect(r.decidedBySource).toBe(S.MANUAL_ADMIN);
    expect(r.status).toBe(St.PRESENT);
  });
});

describe('resolveAttendanceStatus — one event per source (latest replaces)', () => {
  it('a later event for the same source replaces the earlier one in the log entirely', () => {
    // Only the latest MANUAL_RM entry should ever be present for a given source —
    // this simulates the log as the write path actually maintains it (replace, not append).
    const r = resolveAttendanceStatus([ev(S.MANUAL_RM, St.ABSENT, 42)]);
    expect(r).toEqual({ status: St.ABSENT, decidedBySource: S.MANUAL_RM });
  });

  it('if duplicate entries for a source ever land in the log, the latest by occurredAt wins', () => {
    const r = resolveAttendanceStatus([ev(S.MANUAL_RM, St.PRESENT, 42), ev(S.MANUAL_RM, St.ABSENT, 42)]);
    expect(r).toEqual({ status: St.ABSENT, decidedBySource: S.MANUAL_RM });
  });
});

describe('undo (PRD BR-SATT-006, dec-09 fallback) — removing a source\'s mark', () => {
  it('removing the deciding coordinator mark falls back to the next-ranked (RM)', () => {
    const rmAbsent = ev(S.MANUAL_RM, St.ABSENT, 42);
    const zoomPresent = ev(S.ZOOM_WEBHOOK, St.PRESENT);
    // Coordinator's mark removed — as if undone — leaving RM + Zoom.
    const log = [zoomPresent, rmAbsent];
    const r = resolveAttendanceStatus(log);
    expect(r).toEqual({ status: St.ABSENT, decidedBySource: S.MANUAL_RM });
  });

  it('removing every record → UNKNOWN', () => {
    expect(resolveAttendanceStatus([])).toEqual({ status: St.UNKNOWN, decidedBySource: null });
  });
});

describe('activeEvents', () => {
  it('keeps only the latest entry per source', () => {
    const older = ev(S.ZOOM_WEBHOOK, St.PRESENT);
    const newer = ev(S.ZOOM_WEBHOOK, St.ABSENT);
    const active = activeEvents([older, newer]);
    expect(active).toEqual([newer]);
  });

  it('an empty log has no active events', () => {
    expect(activeEvents([])).toHaveLength(0);
  });
});

describe('deriveManualSource (TRD dec-12)', () => {
  it('coordinator role → MANUAL_COORDINATOR', () => {
    expect(deriveManualSource([ROLE_VALUES.COORDINATOR])).toBe(S.MANUAL_COORDINATOR);
  });
  it('admin role → MANUAL_COORDINATOR', () => {
    expect(deriveManualSource([ROLE_VALUES.ADMIN])).toBe(S.MANUAL_COORDINATOR);
  });
  it('RM role → MANUAL_RM', () => {
    expect(deriveManualSource([ROLE_VALUES.RELATIONAL_MANAGER])).toBe(S.MANUAL_RM);
  });
  it('coordinator wins when holding both coordinator and RM', () => {
    expect(deriveManualSource([ROLE_VALUES.RELATIONAL_MANAGER, ROLE_VALUES.COORDINATOR])).toBe(
      S.MANUAL_COORDINATOR,
    );
  });
  it('unrelated role → null', () => {
    expect(deriveManualSource(['viewer'])).toBeNull();
    expect(deriveManualSource(undefined)).toBeNull();
  });
});

describe('resolveManualActor (records the active role)', () => {
  it('coordinator role → coordinator source + shoba role', () => {
    expect(resolveManualActor([ROLE_VALUES.COORDINATOR])).toEqual({
      source: S.MANUAL_COORDINATOR,
      role: ROLE_VALUES.COORDINATOR,
    });
  });
  it('RM role → RM source + relational_manager role', () => {
    expect(resolveManualActor([ROLE_VALUES.RELATIONAL_MANAGER])).toEqual({
      source: S.MANUAL_RM,
      role: ROLE_VALUES.RELATIONAL_MANAGER,
    });
  });
  it('admin role → coordinator source + admin role', () => {
    expect(resolveManualActor([ROLE_VALUES.ADMIN])).toEqual({
      source: S.MANUAL_COORDINATOR,
      role: ROLE_VALUES.ADMIN,
    });
  });
  it('when both coordinator and RM are present, coordinator wins and is the recorded role', () => {
    expect(resolveManualActor([ROLE_VALUES.RELATIONAL_MANAGER, ROLE_VALUES.COORDINATOR])).toEqual({
      source: S.MANUAL_COORDINATOR,
      role: ROLE_VALUES.COORDINATOR,
    });
  });
  it('no qualifying role → null', () => {
    expect(resolveManualActor(['viewer'])).toBeNull();
    expect(resolveManualActor(undefined)).toBeNull();
  });
});

describe('isAttendedFromStatus', () => {
  it('present → true; absent/unknown → false', () => {
    expect(isAttendedFromStatus(St.PRESENT)).toBe(true);
    expect(isAttendedFromStatus(St.ABSENT)).toBe(false);
    expect(isAttendedFromStatus(St.UNKNOWN)).toBe(false);
  });
});

describe('activeMarkFrom — a single source\'s own mark, independent of the resolved winner', () => {
  it('null when that source never marked', () => {
    expect(activeMarkFrom([ev(S.JOIN_CLICK, St.PRESENT)], S.ZOOM_WEBHOOK)).toBeNull();
    expect(activeMarkFrom([], S.MANUAL_RM)).toBeNull();
    expect(activeMarkFrom(undefined, S.MANUAL_RM)).toBeNull();
  });

  it("reports true/false from that source's own event, regardless of which source resolveAttendanceStatus picked", () => {
    const events = [ev(S.ZOOM_WEBHOOK, St.PRESENT), ev(S.MANUAL_COORDINATOR, St.ABSENT)];
    // Coordinator outranks Zoom for the resolved status...
    expect(resolveAttendanceStatus(events).status).toBe(St.ABSENT);
    // ...but Zoom's own mark still reports true, and Coordinator's own mark reports false.
    expect(activeMarkFrom(events, S.ZOOM_WEBHOOK)).toBe(true);
    expect(activeMarkFrom(events, S.MANUAL_COORDINATOR)).toBe(false);
  });

  it('null once that source\'s mark has been removed (undone)', () => {
    // No MANUAL_RM entry left in the log — equivalent to having been undone.
    expect(activeMarkFrom([ev(S.ZOOM_WEBHOOK, St.PRESENT)], S.MANUAL_RM)).toBeNull();
  });

  it('uses the latest entry when duplicates for the same source land in the log', () => {
    const events = [ev(S.MANUAL_RM, St.PRESENT), ev(S.MANUAL_RM, St.ABSENT)];
    expect(activeMarkFrom(events, S.MANUAL_RM)).toBe(false);
  });
});
