/**
 * Unit tests for IamBootstrapSeeder.
 *
 * SYNTHESIZED — no real seeder spec existed before this file.
 * Reconcile examples when more bootstrap seeder tests land.
 *
 * These tests mock every injected service so we never hit the DB.
 * The decorator-fires-in-TestingModule gotcha applies to IamCreateUserHandler
 * (it carries @ApplySchema + @EmitEvent), so we provide EventEmitter2 and
 * assert on bcrypt-hashed password shapes.
 */

import { EventEmitter2 } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';

// Import the handlers/services the seeder depends on
import {
  IamCountAccountService,
  IamCreateAccountService,
  IamFindAccountService,
  IamUpdateAccountByIdService,
} from '@app/iam/account';
import { IamGetPermissionsService } from '@app/iam/permission';
import {
  IamCreatePermissionsRolesService,
  IamGetPermissionsRolesService,
} from '@app/iam/permission-role';
import { IamCreateRoleService, IamFindRoleService, IamGetRolesService } from '@app/iam/role';
import { IamCreateRoleAccountService, IamFindRoleAccountByIdService } from '@app/iam/role-account';
import { IamCreateUserHandler } from '@app/iam/user';

import { BOOTSTRAP_ADMIN_ACCOUNT_ID, IamBootstrapSeeder } from './iam-bootstrap.seeder';

// Re-exported from the seeder for test assertions
const ROLE_ID = process.env.BOOTSTRAP_ADMIN_ROLE_ID ?? 'a0000000-0000-4000-8000-000000000003';

describe('IamBootstrapSeeder', () => {
  let seeder: IamBootstrapSeeder;

  // Mock services
  const findAccount = { main: jest.fn() };
  const countAccount = { main: jest.fn() };
  const createAccount = { main: jest.fn() };
  const updateAccount = { main: jest.fn() };
  const createUser = { main: jest.fn() };
  const findRole = { main: jest.fn() };
  const createRole = { main: jest.fn() };
  const getPermissions = { main: jest.fn() };
  const getPermissionsRoles = { main: jest.fn() };
  const createPermissionsRoles = { main: jest.fn() };
  const findRoleAccountById = { main: jest.fn() };
  const createRoleAccount = { main: jest.fn() };
  const getRoles = { main: jest.fn() };
  // MANDATORY — @EmitEvent inside IamCreateUserHandler reads this.eventEmitter
  const eventEmitter = { emit: jest.fn() };

  const buildModule = async (): Promise<void> => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        IamBootstrapSeeder,
        { provide: IamFindAccountService, useValue: findAccount },
        { provide: IamCountAccountService, useValue: countAccount },
        { provide: IamCreateAccountService, useValue: createAccount },
        { provide: IamUpdateAccountByIdService, useValue: updateAccount },
        { provide: IamCreateUserHandler, useValue: createUser },
        { provide: IamFindRoleService, useValue: findRole },
        { provide: IamCreateRoleService, useValue: createRole },
        { provide: IamGetPermissionsService, useValue: getPermissions },
        { provide: IamGetPermissionsRolesService, useValue: getPermissionsRoles },
        { provide: IamCreatePermissionsRolesService, useValue: createPermissionsRoles },
        { provide: IamFindRoleAccountByIdService, useValue: findRoleAccountById },
        { provide: IamCreateRoleAccountService, useValue: createRoleAccount },
        { provide: IamGetRolesService, useValue: getRoles },
        { provide: EventEmitter2, useValue: eventEmitter },
      ],
    }).compile();

    seeder = module.get<IamBootstrapSeeder>(IamBootstrapSeeder);
  };

  beforeAll(async () => {
    await buildModule();
  });

  beforeEach(() => {
    jest.clearAllMocks();
    // Ensure seeder runs (non-production env)
    process.env.NODE_ENV = 'test';
    delete process.env.BOOTSTRAP_SEED;
  });

  afterAll(() => {
    delete process.env.BOOTSTRAP_SEED;
  });

  // ─── (a) 2nd-boot idempotency ────────────────────────────────────────────

  describe('2nd-boot idempotency', () => {
    it('dispatches NO create calls when all entities already exist', async () => {
      // All find/get services return existing values
      findRole.main.mockResolvedValue({ id: ROLE_ID });
      findAccount.main.mockResolvedValue({
        id: BOOTSTRAP_ADMIN_ACCOUNT_ID,
        dPermissions: { all: ['perm.one', 'perm.two'] },
      });
      getPermissions.main.mockResolvedValue([
        { id: 'p-1', name: 'perm.one' },
        { id: 'p-2', name: 'perm.two' },
      ]);
      getPermissionsRoles.main.mockResolvedValue([
        { permissionId: 'p-1', roleId: ROLE_ID },
        { permissionId: 'p-2', roleId: ROLE_ID },
      ]);
      findRoleAccountById.main.mockResolvedValue({
        roleId: ROLE_ID,
        accountId: BOOTSTRAP_ADMIN_ACCOUNT_ID,
      });
      getRoles.main.mockResolvedValue([
        {
          id: ROLE_ID,
          permissions: [{ name: 'perm.one' }, { name: 'perm.two' }],
        },
      ]);

      await seeder.onApplicationBootstrap();

      expect(createRole.main).not.toHaveBeenCalled();
      expect(createAccount.main).not.toHaveBeenCalled();
      expect(createUser.main).not.toHaveBeenCalled();
      expect(createPermissionsRoles.main).not.toHaveBeenCalled();
      expect(createRoleAccount.main).not.toHaveBeenCalled();
      expect(updateAccount.main).not.toHaveBeenCalled();
    });
  });

  // ─── (b) password path — IamCreateUserHandler ───────────────────────────

  describe('password hashing path', () => {
    it('calls IamCreateUserHandler.main (NOT the raw service) so @ApplySchema hashes the password', async () => {
      findRole.main.mockResolvedValue(null);
      createRole.main.mockResolvedValue(undefined);
      findAccount.main
        .mockResolvedValueOnce(null) // ID lookup → bootstrap account absent
        .mockResolvedValueOnce(null) // email lookup inside seedAccountAndUser → not found
        .mockResolvedValueOnce({ id: BOOTSTRAP_ADMIN_ACCOUNT_ID, dPermissions: {} }); // denormalize lookup
      countAccount.main.mockResolvedValue(0); // fresh system — no accounts yet
      createAccount.main.mockResolvedValue(undefined);
      createUser.main.mockResolvedValue({ id: 'user-id' });
      getPermissions.main.mockResolvedValue([{ id: 'p-1', name: 'perm.one' }]);
      getPermissionsRoles.main.mockResolvedValue([]);
      createPermissionsRoles.main.mockResolvedValue(undefined);
      findRoleAccountById.main.mockResolvedValue(null);
      createRoleAccount.main.mockResolvedValue(undefined);
      getRoles.main.mockResolvedValue([{ id: ROLE_ID, permissions: [{ name: 'perm.one' }] }]);
      updateAccount.main.mockResolvedValue(undefined);

      await seeder.onApplicationBootstrap();

      // The handler is called — not the service directly
      expect(createUser.main).toHaveBeenCalledTimes(1);
      // Payload contains the plaintext password; @ApplySchema fires inside TestingModule
      // so the actual argument may be hashed — we assert on identity, not hash value,
      // because this is a mock (decorator fires on the real handler class, not the mock).
      expect(createUser.main).toHaveBeenCalledWith(
        expect.objectContaining({ id: expect.any(String), accountId: BOOTSTRAP_ADMIN_ACCOUNT_ID }),
        expect.anything(),
      );
    });
  });

  // ─── (c) permission-role attach runs unconditionally ────────────────────

  describe('permission-role attachment — unconditional + idempotent', () => {
    const allPermissions = [
      { id: 'p-1', name: 'perm.one' },
      { id: 'p-2', name: 'perm.two' },
      { id: 'p-3', name: 'perm.three' },
    ];

    const setupCommon = () => {
      findRole.main.mockResolvedValue({ id: ROLE_ID });
      findAccount.main.mockResolvedValue({
        id: BOOTSTRAP_ADMIN_ACCOUNT_ID,
        dPermissions: { all: [] },
      });
      getPermissions.main.mockResolvedValue(allPermissions);
      findRoleAccountById.main.mockResolvedValue({
        roleId: ROLE_ID,
        accountId: BOOTSTRAP_ADMIN_ACCOUNT_ID,
      });
      getRoles.main.mockResolvedValue([
        { id: ROLE_ID, permissions: allPermissions.map((p) => ({ name: p.name })) },
      ]);
      updateAccount.main.mockResolvedValue(undefined);
    };

    it('inserts only the MISSING rows — p-1 already attached, inserts p-2 and p-3', async () => {
      setupCommon();
      getPermissionsRoles.main.mockResolvedValue([{ permissionId: 'p-1', roleId: ROLE_ID }]);
      createPermissionsRoles.main.mockResolvedValue(undefined);

      await seeder.onApplicationBootstrap();

      expect(createPermissionsRoles.main).toHaveBeenCalledTimes(1);
      expect(createPermissionsRoles.main).toHaveBeenCalledWith(
        expect.arrayContaining([
          { permissionId: 'p-2', roleId: ROLE_ID },
          { permissionId: 'p-3', roleId: ROLE_ID },
        ]),
      );
      // p-1 must NOT be in the call (it already existed)
      const insertedRows: Array<{ permissionId: string; roleId: string }> =
        createPermissionsRoles.main.mock.calls[0][0];
      expect(insertedRows.map((r) => r.permissionId)).not.toContain('p-1');
    });

    it('does NOT call createPermissionsRoles when all rows are already attached', async () => {
      setupCommon();
      getPermissionsRoles.main.mockResolvedValue(
        allPermissions.map((p) => ({ permissionId: p.id, roleId: ROLE_ID })),
      );

      await seeder.onApplicationBootstrap();

      expect(createPermissionsRoles.main).not.toHaveBeenCalled();
    });

    it('runs seedPermissionRoles even when the role already existed (roleIsNew gate removed)', async () => {
      // Role pre-exists — old code would skip permission-role seeding
      findRole.main.mockResolvedValue({ id: ROLE_ID });
      findAccount.main.mockResolvedValue({
        id: BOOTSTRAP_ADMIN_ACCOUNT_ID,
        dPermissions: { all: [] },
      });
      getPermissions.main.mockResolvedValue([{ id: 'p-new', name: 'perm.new' }]);
      getPermissionsRoles.main.mockResolvedValue([]); // missing row
      createPermissionsRoles.main.mockResolvedValue(undefined);
      findRoleAccountById.main.mockResolvedValue({
        roleId: ROLE_ID,
        accountId: BOOTSTRAP_ADMIN_ACCOUNT_ID,
      });
      getRoles.main.mockResolvedValue([{ id: ROLE_ID, permissions: [{ name: 'perm.new' }] }]);
      updateAccount.main.mockResolvedValue(undefined);

      await seeder.onApplicationBootstrap();

      // createPermissionsRoles MUST be called even though the role already existed
      expect(createPermissionsRoles.main).toHaveBeenCalledTimes(1);
    });
  });

  // ─── (d) denormalizeAccountPermissions recomputes every boot ────────────

  describe('denormalizeAccountPermissions — write-if-different', () => {
    it('updates dPermissions when the count differs from stored value', async () => {
      findRole.main.mockResolvedValue({ id: ROLE_ID });
      findAccount.main.mockResolvedValue({
        id: BOOTSTRAP_ADMIN_ACCOUNT_ID,
        // Stored: only 1 permission — but role now has 2
        dPermissions: { all: ['perm.old'] },
      });
      getPermissions.main.mockResolvedValue([
        { id: 'p-1', name: 'perm.old' },
        { id: 'p-2', name: 'perm.new' },
      ]);
      getPermissionsRoles.main.mockResolvedValue([
        { permissionId: 'p-1', roleId: ROLE_ID },
        { permissionId: 'p-2', roleId: ROLE_ID },
      ]);
      findRoleAccountById.main.mockResolvedValue({
        roleId: ROLE_ID,
        accountId: BOOTSTRAP_ADMIN_ACCOUNT_ID,
      });
      getRoles.main.mockResolvedValue([
        {
          id: ROLE_ID,
          permissions: [{ name: 'perm.old' }, { name: 'perm.new' }],
        },
      ]);
      updateAccount.main.mockResolvedValue(undefined);

      await seeder.onApplicationBootstrap();

      expect(updateAccount.main).toHaveBeenCalledTimes(1);
      // Destructure actual call args for surgical assertions
      const [updatePayload, updateConstraint, updateMeta] = updateAccount.main.mock.calls[0];
      expect(updatePayload.id).toBe(BOOTSTRAP_ADMIN_ACCOUNT_ID);
      expect(updatePayload.dPermissions.all).toEqual(
        expect.arrayContaining(['perm.old', 'perm.new']),
      );
      expect(updatePayload.dPermissions.all).toHaveLength(2);
      expect(updateConstraint).toBeUndefined();
      expect(updateMeta).toMatchObject({ repositoryOptions: { auditing: null } });
    });

    it('skips update when stored dPermissions.all count matches computed count', async () => {
      findRole.main.mockResolvedValue({ id: ROLE_ID });
      findAccount.main.mockResolvedValue({
        id: BOOTSTRAP_ADMIN_ACCOUNT_ID,
        dPermissions: { all: ['perm.one', 'perm.two'] },
      });
      getPermissions.main.mockResolvedValue([
        { id: 'p-1', name: 'perm.one' },
        { id: 'p-2', name: 'perm.two' },
      ]);
      getPermissionsRoles.main.mockResolvedValue([
        { permissionId: 'p-1', roleId: ROLE_ID },
        { permissionId: 'p-2', roleId: ROLE_ID },
      ]);
      findRoleAccountById.main.mockResolvedValue({
        roleId: ROLE_ID,
        accountId: BOOTSTRAP_ADMIN_ACCOUNT_ID,
      });
      getRoles.main.mockResolvedValue([
        {
          id: ROLE_ID,
          permissions: [{ name: 'perm.one' }, { name: 'perm.two' }],
        },
      ]);

      await seeder.onApplicationBootstrap();

      expect(updateAccount.main).not.toHaveBeenCalled();
    });
  });

  // ─── run-once guard (empty-system gate) ──────────────────────────────────

  describe('run-once guard', () => {
    it('(a) fresh system: accounts table EMPTY + bootstrap account absent → create services ARE called', async () => {
      // Bootstrap account not found by ID
      findAccount.main.mockResolvedValueOnce(null);
      // No accounts at all in the system
      countAccount.main.mockResolvedValue(0);
      // Role does not exist yet
      findRole.main.mockResolvedValue(null);
      createRole.main.mockResolvedValue(undefined);
      // email lookup inside seedAccountAndUser → not found
      findAccount.main.mockResolvedValueOnce(null);
      createAccount.main.mockResolvedValue(undefined);
      createUser.main.mockResolvedValue({ id: 'user-id' });
      getPermissions.main.mockResolvedValue([{ id: 'p-1', name: 'perm.one' }]);
      getPermissionsRoles.main.mockResolvedValue([]);
      createPermissionsRoles.main.mockResolvedValue(undefined);
      findRoleAccountById.main.mockResolvedValue(null);
      createRoleAccount.main.mockResolvedValue(undefined);
      // denormalize: account now exists
      findAccount.main.mockResolvedValueOnce({
        id: BOOTSTRAP_ADMIN_ACCOUNT_ID,
        dPermissions: {},
      });
      getRoles.main.mockResolvedValue([{ id: ROLE_ID, permissions: [{ name: 'perm.one' }] }]);
      updateAccount.main.mockResolvedValue(undefined);

      await seeder.onApplicationBootstrap();

      expect(createRole.main).toHaveBeenCalledTimes(1);
      expect(createAccount.main).toHaveBeenCalledTimes(1);
      expect(createUser.main).toHaveBeenCalledTimes(1);
    });

    it('(b) initialized system: accounts count > 0 + bootstrap account ABSENT → early return, NO resurrection', async () => {
      // Bootstrap account not found by ID
      findAccount.main.mockResolvedValue(null);
      // Other accounts already exist
      countAccount.main.mockResolvedValue(3);

      await seeder.onApplicationBootstrap();

      // None of the create services may be called
      expect(createRole.main).not.toHaveBeenCalled();
      expect(createAccount.main).not.toHaveBeenCalled();
      expect(createUser.main).not.toHaveBeenCalled();
      expect(createRoleAccount.main).not.toHaveBeenCalled();
      // Reconciliation must not run either
      expect(getPermissions.main).not.toHaveBeenCalled();
      expect(createPermissionsRoles.main).not.toHaveBeenCalled();
      expect(updateAccount.main).not.toHaveBeenCalled();
    });

    it('(c) bootstrap account EXISTS → create skipped, but seedPermissionRoles + denormalize DO run', async () => {
      // Bootstrap account found by ID
      findAccount.main
        .mockResolvedValueOnce({ id: BOOTSTRAP_ADMIN_ACCOUNT_ID, dPermissions: { all: [] } })
        // denormalize lookup
        .mockResolvedValueOnce({ id: BOOTSTRAP_ADMIN_ACCOUNT_ID, dPermissions: { all: [] } });
      // Role already exists
      findRole.main.mockResolvedValue({ id: ROLE_ID });
      getPermissions.main.mockResolvedValue([{ id: 'p-1', name: 'perm.one' }]);
      getPermissionsRoles.main.mockResolvedValue([]);
      createPermissionsRoles.main.mockResolvedValue(undefined);
      findRoleAccountById.main.mockResolvedValue({
        roleId: ROLE_ID,
        accountId: BOOTSTRAP_ADMIN_ACCOUNT_ID,
      });
      getRoles.main.mockResolvedValue([{ id: ROLE_ID, permissions: [{ name: 'perm.one' }] }]);
      updateAccount.main.mockResolvedValue(undefined);

      await seeder.onApplicationBootstrap();

      // Create services must NOT be called
      expect(createAccount.main).not.toHaveBeenCalled();
      expect(createUser.main).not.toHaveBeenCalled();
      // countAccount must NOT be called (bootstrap account exists)
      expect(countAccount.main).not.toHaveBeenCalled();
      // Reconciliation MUST run
      expect(getPermissions.main).toHaveBeenCalledTimes(1);
      expect(createPermissionsRoles.main).toHaveBeenCalledTimes(1);
      expect(updateAccount.main).toHaveBeenCalledTimes(1);
    });
  });

  // ─── gate disabled ───────────────────────────────────────────────────────

  describe('gate disabled', () => {
    it('does nothing when NODE_ENV=production and BOOTSTRAP_SEED is not set', async () => {
      process.env.NODE_ENV = 'production';
      delete process.env.BOOTSTRAP_SEED;

      await seeder.onApplicationBootstrap();

      expect(findRole.main).not.toHaveBeenCalled();
      expect(createRole.main).not.toHaveBeenCalled();
      expect(findAccount.main).not.toHaveBeenCalled();
    });
  });
});
