import { describe, expect, it } from 'vitest';
import type { Principal, Scope } from './authorization.types';
import {
  principalCoversResource,
  principalHasAnyPrivilege,
  principalHasAnyRole,
} from './principal-matchers';

function makePrincipal(overrides: Partial<Principal> = {}): Principal {
  return {
    user: {
      id: 'user-1',
      personId: 'person-1',
      entraOid: 'oid-1',
      tenantId: 'tenant-1',
      displayName: 'Alice',
    },
    privileges: [],
    roles: [],
    scopes: [],
    amr: ['pwd', 'mfa'],
    ...overrides,
  };
}

describe('principalHasAnyPrivilege', () => {
  it('returns true when the principal carries one of the required privileges', () => {
    const p = makePrincipal({ privileges: ['Portal.Admin'] });
    expect(principalHasAnyPrivilege(p, ['Portal.Admin'])).toBe(true);
  });

  it('OR-combines multiple required privileges', () => {
    const p = makePrincipal({ privileges: ['Portal.Auditor'] });
    expect(principalHasAnyPrivilege(p, ['Portal.Admin', 'Portal.Auditor'])).toBe(true);
  });

  it('returns false when none of the required privileges is held', () => {
    const p = makePrincipal({ privileges: ['Portal.DPO'] });
    expect(principalHasAnyPrivilege(p, ['Portal.Admin'])).toBe(false);
  });

  it('returns false when the principal has no privileges at all', () => {
    const p = makePrincipal();
    expect(principalHasAnyPrivilege(p, ['Portal.Admin'])).toBe(false);
  });

  // An empty requirement list is a degenerate case: a guard
  // configured with no privilege constraint is equivalent to no
  // guard at all. Document the contract so a caller cannot
  // accidentally lock everyone out by passing an empty array.
  it('returns true on an empty requirement list (no constraint)', () => {
    const p = makePrincipal();
    expect(principalHasAnyPrivilege(p, [])).toBe(true);
  });
});

describe('principalHasAnyRole', () => {
  it('returns true when the principal carries one of the required roles', () => {
    const p = makePrincipal({ roles: ['rh'] });
    expect(principalHasAnyRole(p, ['rh'])).toBe(true);
  });

  it('OR-combines multiple required roles', () => {
    const p = makePrincipal({ roles: ['comptable'] });
    expect(principalHasAnyRole(p, ['rh', 'comptable', 'responsable-paie'])).toBe(true);
  });

  it('returns false when none of the required roles is held', () => {
    const p = makePrincipal({ roles: ['collaborateur'] });
    expect(principalHasAnyRole(p, ['directeur-etablissement'])).toBe(false);
  });

  it('returns true on an empty requirement list (no constraint)', () => {
    const p = makePrincipal({ roles: ['collaborateur'] });
    expect(principalHasAnyRole(p, [])).toBe(true);
  });
});

describe('principalCoversResource — unrestricted', () => {
  it('covers every resource regardless of its fields', () => {
    const p = makePrincipal({ scopes: [{ kind: 'unrestricted' }] });
    expect(principalCoversResource(p, {})).toBe(true);
    expect(principalCoversResource(p, { etablissementFiness: '0330800013' })).toBe(true);
    expect(principalCoversResource(p, { isSiege: true })).toBe(true);
  });
});

describe('principalCoversResource — self', () => {
  it('covers a resource whose personId matches the principal user', () => {
    const p = makePrincipal({ scopes: [{ kind: 'self' }] });
    expect(principalCoversResource(p, { personId: 'person-1' })).toBe(true);
  });

  it('does not cover a resource owned by a different Person', () => {
    const p = makePrincipal({ scopes: [{ kind: 'self' }] });
    expect(principalCoversResource(p, { personId: 'person-2' })).toBe(false);
  });

  it('does not cover a resource that has no personId (collective resource)', () => {
    const p = makePrincipal({ scopes: [{ kind: 'self' }] });
    expect(principalCoversResource(p, { etablissementFiness: '0330800013' })).toBe(false);
  });
});

describe('principalCoversResource — etablissement / delegation / region', () => {
  it('etablissement scope covers a resource with the matching FINESS', () => {
    const p = makePrincipal({
      scopes: [{ kind: 'etablissement', value: '0330800013' }],
    });
    expect(principalCoversResource(p, { etablissementFiness: '0330800013' })).toBe(true);
  });

  it('etablissement scope does not cover a different FINESS', () => {
    const p = makePrincipal({
      scopes: [{ kind: 'etablissement', value: '0330800013' }],
    });
    expect(principalCoversResource(p, { etablissementFiness: '0330800021' })).toBe(false);
  });

  it('delegation scope covers a resource in the same department', () => {
    const p = makePrincipal({
      scopes: [{ kind: 'delegation', value: '33' }],
    });
    expect(principalCoversResource(p, { delegationCode: '33' })).toBe(true);
  });

  it('region scope covers a resource in the same region', () => {
    const p = makePrincipal({
      scopes: [{ kind: 'region', value: '75' }],
    });
    expect(principalCoversResource(p, { regionCode: '75' })).toBe(true);
  });

  // The matcher is intentionally non-transitive on the parentage
  // chain: a `delegation:33` scope does NOT cover an etablissement
  // unless the etablissement's `delegationCode` is supplied. The
  // expansion lives in the caller (which knows the parentage of
  // the resource it is protecting); the matcher just compares
  // what the caller supplies.
  it('delegation scope covers an etablissement when the etablissement carries delegationCode', () => {
    const p = makePrincipal({
      scopes: [{ kind: 'delegation', value: '33' }],
    });
    expect(
      principalCoversResource(p, {
        etablissementFiness: '0330800013',
        delegationCode: '33',
      }),
    ).toBe(true);
  });

  it('delegation scope does NOT cover an etablissement that omits delegationCode', () => {
    const p = makePrincipal({
      scopes: [{ kind: 'delegation', value: '33' }],
    });
    expect(principalCoversResource(p, { etablissementFiness: '0330800013' })).toBe(false);
  });
});

describe('principalCoversResource — siege', () => {
  it('covers a resource flagged isSiege: true', () => {
    const p = makePrincipal({ scopes: [{ kind: 'siege' }] });
    expect(principalCoversResource(p, { isSiege: true })).toBe(true);
  });

  it('does not cover a non-siege resource', () => {
    const p = makePrincipal({ scopes: [{ kind: 'siege' }] });
    expect(principalCoversResource(p, { delegationCode: '75' })).toBe(false);
  });
});

describe('principalCoversResource — composition', () => {
  it('returns true on the first matching scope (OR-combine)', () => {
    const p = makePrincipal({
      scopes: [
        { kind: 'etablissement', value: '0330800013' },
        { kind: 'delegation', value: '75' },
      ],
    });
    expect(principalCoversResource(p, { delegationCode: '75' })).toBe(true);
  });

  it('returns false when an empty scope list — guards must deny safely on no coverage', () => {
    const p = makePrincipal({ scopes: [] });
    expect(principalCoversResource(p, { etablissementFiness: '0330800013' })).toBe(false);
  });

  it('exhaustive switch — every scope kind is handled (compile-time check)', () => {
    // Iterate every kind by constructing one scope of each, with
    // matching resource data. The matcher must return true for
    // each; a missing case in the switch would fail at least one.
    const scopes: Scope[] = [
      { kind: 'unrestricted' },
      { kind: 'self' },
      { kind: 'etablissement', value: 'f1' },
      { kind: 'delegation', value: '33' },
      { kind: 'region', value: '75' },
      { kind: 'siege' },
    ];
    for (const scope of scopes) {
      const p = makePrincipal({ scopes: [scope] });
      const resource = {
        personId: 'person-1',
        etablissementFiness: 'f1',
        delegationCode: '33',
        regionCode: '75',
        isSiege: true,
      };
      expect(principalCoversResource(p, resource)).toBe(true);
    }
  });
});
