import { IamAccount, IamAccountTypeEnum, IamCreateAccountInput } from '@api/graphql';
import {
  IamCreateAccountService,
  IamFindAccountByIdService,
  IamGetAccountsService,
} from '@app/iam/account';
import { IamGetRolesService } from '@app/iam/role';
import { IamPermissions } from '@app/iam/shared';
import { IamGetTenantsService } from '@app/iam/tenant';
import { IamCreateUserService } from '@app/iam/user';
import { IAM_CLIENT_READER } from '@bridges/iam/client/ports';
import { BadRequestException, ConflictException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';
import { IamCreateAccountHandler } from './iam-create-account.handler';

const ACCOUNT_ID = 'a1111111-a111-4111-8111-111111111111';
const CLIENT_ID = 'c1111111-c111-4111-8111-111111111111';
const ROLE_ID = 'b1111111-b111-4111-8111-111111111111';
const CALLER_ID = 'd0000000-0000-4000-8000-000000000000';
const T_ROOT = '11111111-1111-4111-8111-111111111111';
const T_CHILD_1 = '11111111-1111-4111-8111-111111111112';
const T_CHILD_2 = '11111111-1111-4111-8111-111111111113';
const T_OTHER = '22222222-2222-4222-8222-222222222222';
const COLLIDER_ID = 'f0000000-f000-4000-8000-000000000000';

const buildPayload = (overrides: Partial<IamCreateAccountInput> = {}): IamCreateAccountInput => ({
  id: ACCOUNT_ID,
  type: IamAccountTypeEnum.SERVICE,
  code: null,
  email: 'service@example.com',
  username: 'service-account',
  isActive: true,
  clientId: CLIENT_ID,
  tags: null,
  scopes: null,
  dApplicationCodes: [],
  dPermissions: {},
  dTenants: [],
  meta: null,
  roleIds: [],
  tenantIds: [],
  ...overrides,
});

const buildCurrentAccount = (overrides: Partial<IamAccount> = {}): IamAccount =>
  ({
    id: CALLER_ID,
    dPermissions: { all: ['p1', 'p2', 'p3', IamPermissions.SUDO] },
    ...overrides,
  }) as unknown as IamAccount;

describe('IamCreateAccountHandler', () => {
  let handler: IamCreateAccountHandler;
  let createService: { main: jest.Mock };
  let findByIdService: { main: jest.Mock };
  let getAccountsService: { main: jest.Mock };
  let getRolesService: { main: jest.Mock };
  let getTenantsService: { main: jest.Mock };
  let clientReader: { findByIdWithApplications: jest.Mock };
  let userCreateService: { main: jest.Mock };
  let eventEmitter: { emit: jest.Mock };

  beforeEach(async () => {
    createService = { main: jest.fn().mockResolvedValue(undefined) };
    findByIdService = { main: jest.fn().mockResolvedValue({ id: ACCOUNT_ID }) };
    getAccountsService = { main: jest.fn().mockResolvedValue([]) };
    getRolesService = { main: jest.fn().mockResolvedValue([]) };
    getTenantsService = { main: jest.fn().mockResolvedValue([]) };
    clientReader = {
      findByIdWithApplications: jest
        .fn()
        .mockResolvedValue({ id: CLIENT_ID, applicationCodes: ['APP_A'] }),
    };
    userCreateService = { main: jest.fn().mockResolvedValue(undefined) };
    eventEmitter = { emit: jest.fn() };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        IamCreateAccountHandler,
        { provide: EventEmitter2, useValue: eventEmitter },
        { provide: IamCreateAccountService, useValue: createService },
        { provide: IamFindAccountByIdService, useValue: findByIdService },
        { provide: IamGetAccountsService, useValue: getAccountsService },
        { provide: IamGetRolesService, useValue: getRolesService },
        { provide: IamGetTenantsService, useValue: getTenantsService },
        { provide: IAM_CLIENT_READER, useValue: clientReader },
        { provide: IamCreateUserService, useValue: userCreateService },
      ],
    }).compile();

    handler = module.get<IamCreateAccountHandler>(IamCreateAccountHandler);
  });

  it('creates a SERVICE account without invoking user service', async () => {
    await handler.main(buildCurrentAccount(), buildPayload());

    expect(createService.main).toHaveBeenCalledTimes(1);
    expect(userCreateService.main).not.toHaveBeenCalled();
  });

  it('creates a USER account invoking both services with shared operationId', async () => {
    const payload = buildPayload({
      type: IamAccountTypeEnum.USER,
      user: {
        id: ACCOUNT_ID,
        accountId: ACCOUNT_ID,
        name: 'Jane',
        surname: 'Doe',
        avatar: null,
        mobile: null,
        langId: null,
        password: 'Abc12345!',
        isTwoFactorAuthenticationEnabled: false,
        twoFactorAuthenticationSecret: null,
        rememberToken: null,
        meta: null,
      },
    });

    await handler.main(buildCurrentAccount(), payload);

    expect(createService.main).toHaveBeenCalledTimes(1);
    expect(userCreateService.main).toHaveBeenCalledTimes(1);

    const accountMeta = createService.main.mock.calls[0][1];
    const userMeta = userCreateService.main.mock.calls[0][1];

    expect(accountMeta.repositoryOptions.auditing.operationSort).toBe(1);
    expect(userMeta.repositoryOptions.auditing.operationSort).toBe(2);
    expect(accountMeta.repositoryOptions.auditing.operationId).toBe(
      userMeta.repositoryOptions.auditing.operationId,
    );

    const userPayload = userCreateService.main.mock.calls[0][0];
    expect(userPayload.accountId).toBe(payload.id);
    expect(userPayload.name).toBe('Jane');
    expect(userPayload.password).toBeDefined();
    expect(userPayload.password).not.toBe('');
  });

  it('rejects USER account creation when user block is missing', async () => {
    const payload = buildPayload({ type: IamAccountTypeEnum.USER, user: null });

    await expect(handler.main(buildCurrentAccount(), payload)).rejects.toBeInstanceOf(
      BadRequestException,
    );
    expect(createService.main).not.toHaveBeenCalled();
    expect(userCreateService.main).not.toHaveBeenCalled();
  });

  it('rejects when email already exists', async () => {
    getAccountsService.main.mockResolvedValueOnce([
      { id: COLLIDER_ID, email: 'service@example.com', username: 'other', code: null },
    ]);

    await expect(handler.main(buildCurrentAccount(), buildPayload())).rejects.toMatchObject({
      response: { translation: 'iam.error.uniqueEmail' },
    });
    expect(createService.main).not.toHaveBeenCalled();
  });

  it('rejects when username already exists', async () => {
    getAccountsService.main.mockResolvedValueOnce([
      { id: COLLIDER_ID, email: 'other@example.com', username: 'service-account', code: null },
    ]);

    await expect(handler.main(buildCurrentAccount(), buildPayload())).rejects.toMatchObject({
      response: { translation: 'iam.error.uniqueUsername' },
    });
  });

  it('rejects when code already exists', async () => {
    const payload = buildPayload({ code: 'CUSTOMER-007' });
    getAccountsService.main.mockResolvedValueOnce([
      { id: COLLIDER_ID, email: 'other@example.com', username: 'other', code: 'CUSTOMER-007' },
    ]);

    await expect(handler.main(buildCurrentAccount(), payload)).rejects.toMatchObject({
      response: { translation: 'iam.error.uniqueCode' },
    });
  });

  it('skips code uniqueness check when payload.code is null', async () => {
    await handler.main(buildCurrentAccount(), buildPayload({ code: null }));

    const callArgs = getAccountsService.main.mock.calls[0][0];
    expect(callArgs.where['[or]']).not.toHaveProperty('code');
  });

  it('overrides dApplicationCodes and dPermissions with computed values', async () => {
    clientReader.findByIdWithApplications.mockResolvedValueOnce({
      id: CLIENT_ID,
      applicationCodes: ['APP_X', 'APP_Y'],
    });
    getRolesService.main.mockResolvedValueOnce([
      { id: ROLE_ID, permissions: [{ name: 'p1' }, { name: 'p2' }] },
    ]);

    const payload = buildPayload({
      roleIds: [ROLE_ID],
      dApplicationCodes: ['SHOULD_BE_DISCARDED'],
      dPermissions: { all: ['SHOULD_BE_DISCARDED'] },
    });

    await handler.main(buildCurrentAccount(), payload);

    const persisted = createService.main.mock.calls[0][0];
    expect(persisted.dApplicationCodes).toEqual(['APP_X', 'APP_Y']);
    expect(persisted.dPermissions).toEqual({
      all: ['p1', 'p2'],
      [ROLE_ID]: ['p1', 'p2'],
    });
  });

  it('rejects when caller without SUDO assigns permissions they do not hold', async () => {
    getRolesService.main.mockResolvedValueOnce([
      { id: ROLE_ID, permissions: [{ name: 'forbidden' }] },
    ]);

    const payload = buildPayload({ roleIds: [ROLE_ID] });
    const caller = buildCurrentAccount({
      dPermissions: { all: ['p1'] },
    });

    await expect(handler.main(caller, payload)).rejects.toMatchObject({
      response: { statusCode: 401, translation: 'iam.error.insufficientPermissions' },
    });
    expect(createService.main).not.toHaveBeenCalled();
  });

  it('allows caller with SUDO to assign any role', async () => {
    getRolesService.main.mockResolvedValueOnce([
      { id: ROLE_ID, permissions: [{ name: 'never-held-by-caller' }] },
    ]);

    const payload = buildPayload({ roleIds: [ROLE_ID] });
    const caller = buildCurrentAccount({
      dPermissions: { all: [IamPermissions.SUDO] },
    });

    await expect(handler.main(caller, payload)).resolves.toBeDefined();
    expect(createService.main).toHaveBeenCalledTimes(1);
  });

  it('expands tenantIds with descendants when hasAddChildTenants is true', async () => {
    getTenantsService.main.mockResolvedValueOnce([
      { id: T_ROOT, parentId: null },
      { id: T_CHILD_1, parentId: T_ROOT },
      { id: T_CHILD_2, parentId: T_ROOT },
      { id: T_OTHER, parentId: null },
    ]);

    const payload = buildPayload({ tenantIds: [T_ROOT], hasAddChildTenants: true });
    await handler.main(buildCurrentAccount(), payload);

    const persisted = createService.main.mock.calls[0][0];
    expect(new Set(persisted.tenantIds)).toEqual(new Set([T_ROOT, T_CHILD_1, T_CHILD_2]));
    expect(new Set(persisted.dTenants)).toEqual(new Set([T_ROOT, T_CHILD_1, T_CHILD_2]));
  });

  it('leaves tenantIds intact when hasAddChildTenants is false', async () => {
    const payload = buildPayload({ tenantIds: [T_ROOT], hasAddChildTenants: false });
    await handler.main(buildCurrentAccount(), payload);

    expect(getTenantsService.main).not.toHaveBeenCalled();
    const persisted = createService.main.mock.calls[0][0];
    expect(persisted.tenantIds).toEqual([T_ROOT]);
  });

  it('emits iam.account.created on success', async () => {
    await handler.main(buildCurrentAccount(), buildPayload());

    expect(eventEmitter.emit).toHaveBeenCalledWith(
      'iam.account.created',
      expect.objectContaining({ payload: expect.objectContaining({ id: ACCOUNT_ID }) }),
    );
  });

  it('does not emit when uniqueness validation fails', async () => {
    getAccountsService.main.mockResolvedValueOnce([
      { id: COLLIDER_ID, email: 'service@example.com', username: 'other', code: null },
    ]);

    await expect(handler.main(buildCurrentAccount(), buildPayload())).rejects.toBeInstanceOf(
      ConflictException,
    );
    expect(eventEmitter.emit).not.toHaveBeenCalled();
  });
});
