import { IamAccount, IamAccountTypeEnum, IamUpdateAccountByIdInput } from '@api/graphql';
import {
  IamFindAccountByIdService,
  IamGetAccountsService,
  IamUpdateAccountByIdService,
} from '@app/iam/account';
import { IamGetRolesService } from '@app/iam/role';
import { IamPermissions } from '@app/iam/shared';
import { IamGetTenantsService } from '@app/iam/tenant';
import { IamUpdateUserByIdService } from '@app/iam/user';
import { IAM_CLIENT_READER } from '@bridges/iam/client/ports';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';
import { IamUpdateAccountByIdHandler } from './iam-update-account-by-id.handler';

const ACCOUNT_ID = 'a1111111-a111-4111-8111-111111111111';
const CLIENT_OLD = 'c0000000-c000-4000-8000-000000000001';
const CLIENT_NEW = 'c1111111-c111-4111-8111-111111111111';
const ROLE_NEW = 'b1111111-b111-4111-8111-111111111111';
const USER_ID = 'e1111111-e111-4111-8111-111111111111';
const COLLIDER_ID = 'f0000000-f000-4000-8000-000000000000';
const CALLER_ID = 'd0000000-0000-4000-8000-000000000000';

const buildExisting = (overrides: Partial<IamAccount> = {}): IamAccount =>
  ({
    id: ACCOUNT_ID,
    type: IamAccountTypeEnum.USER,
    code: 'CODE-OLD',
    email: 'old@example.com',
    username: 'old-user',
    isActive: true,
    clientId: CLIENT_OLD,
    tags: [],
    scopes: [],
    dApplicationCodes: ['APP_OLD'],
    dPermissions: { all: ['p_old'] },
    dTenants: [],
    user: { id: USER_ID, name: 'Old' },
    ...overrides,
  }) as unknown as IamAccount;

const buildPayload = (
  overrides: Partial<IamUpdateAccountByIdInput> = {},
): IamUpdateAccountByIdInput => ({
  id: ACCOUNT_ID,
  ...overrides,
});

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

describe('IamUpdateAccountByIdHandler', () => {
  let handler: IamUpdateAccountByIdHandler;
  let updateByIdService: { 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 userUpdateByIdService: { main: jest.Mock };
  let eventEmitter: { emit: jest.Mock };

  beforeEach(async () => {
    updateByIdService = { main: jest.fn().mockResolvedValue(undefined) };
    findByIdService = { main: jest.fn().mockResolvedValue(buildExisting()) };
    getAccountsService = { main: jest.fn().mockResolvedValue([]) };
    getRolesService = { main: jest.fn().mockResolvedValue([]) };
    getTenantsService = { main: jest.fn().mockResolvedValue([]) };
    clientReader = {
      findByIdWithApplications: jest
        .fn()
        .mockResolvedValue({ id: CLIENT_OLD, applicationCodes: [] }),
    };
    userUpdateByIdService = { main: jest.fn().mockResolvedValue(undefined) };
    eventEmitter = { emit: jest.fn() };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        IamUpdateAccountByIdHandler,
        { provide: EventEmitter2, useValue: eventEmitter },
        { provide: IamUpdateAccountByIdService, useValue: updateByIdService },
        { provide: IamFindAccountByIdService, useValue: findByIdService },
        { provide: IamGetAccountsService, useValue: getAccountsService },
        { provide: IamGetRolesService, useValue: getRolesService },
        { provide: IamGetTenantsService, useValue: getTenantsService },
        { provide: IAM_CLIENT_READER, useValue: clientReader },
        { provide: IamUpdateUserByIdService, useValue: userUpdateByIdService },
      ],
    }).compile();

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

  it('does not refetch client/roles when only email changes', async () => {
    await handler.main(buildCaller(), buildPayload({ email: 'new@example.com' }));

    expect(clientReader.findByIdWithApplications).not.toHaveBeenCalled();
    expect(getRolesService.main).not.toHaveBeenCalled();
    expect(updateByIdService.main).toHaveBeenCalledTimes(1);
  });

  it('recomputes dApplicationCodes when clientId changes', async () => {
    clientReader.findByIdWithApplications.mockResolvedValueOnce({
      id: CLIENT_NEW,
      applicationCodes: ['APP_NEW'],
    });

    await handler.main(buildCaller(), buildPayload({ clientId: CLIENT_NEW }));

    expect(clientReader.findByIdWithApplications).toHaveBeenCalledWith(CLIENT_NEW);
    const persisted = updateByIdService.main.mock.calls[0][0];
    expect(persisted.dApplicationCodes).toEqual(['APP_NEW']);
  });

  it('recomputes dPermissions when roleIds is provided', async () => {
    getRolesService.main.mockResolvedValueOnce([
      { id: ROLE_NEW, permissions: [{ name: 'p_new' }] },
    ]);

    await handler.main(buildCaller(), buildPayload({ roleIds: [ROLE_NEW] }));

    const persisted = updateByIdService.main.mock.calls[0][0];
    expect(persisted.dPermissions).toEqual({ all: ['p_new'], [ROLE_NEW]: ['p_new'] });
  });

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

    await expect(
      handler.main(
        buildCaller({ dPermissions: { all: ['p1'] } }),
        buildPayload({ roleIds: [ROLE_NEW] }),
      ),
    ).rejects.toMatchObject({
      response: { translation: 'iam.error.insufficientPermissions' },
    });
    expect(updateByIdService.main).not.toHaveBeenCalled();
  });

  it('updates user with new password when payload.user.password is non-empty', async () => {
    await handler.main(
      buildCaller(),
      buildPayload({
        user: {
          id: USER_ID,
          name: 'New Name',
          password: 'NewPass1!',
        },
      }),
    );

    expect(userUpdateByIdService.main).toHaveBeenCalledTimes(1);
    const userPayload = userUpdateByIdService.main.mock.calls[0][0];
    expect(userPayload.id).toBe(USER_ID);
    expect(userPayload.name).toBe('New Name');
    expect(userPayload.password).toBeDefined();
    expect(userPayload.password).not.toBe('');
  });

  it('updates user without touching password when payload.user.password is omitted', async () => {
    await handler.main(
      buildCaller(),
      buildPayload({
        user: {
          id: USER_ID,
          name: 'New Name',
        },
      }),
    );

    const userPayload = userUpdateByIdService.main.mock.calls[0][0];
    expect(userPayload.name).toBe('New Name');
    expect(userPayload).not.toHaveProperty('password');
  });

  it('does not invoke user update for SERVICE accounts', async () => {
    findByIdService.main.mockResolvedValueOnce(
      buildExisting({ type: IamAccountTypeEnum.SERVICE, user: null }),
    );

    await handler.main(
      buildCaller(),
      buildPayload({
        user: {
          id: USER_ID,
          name: 'Should be ignored',
        },
      }),
    );

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

  it('rejects when changed email collides with another account', async () => {
    getAccountsService.main.mockResolvedValueOnce([
      { id: COLLIDER_ID, email: 'taken@example.com', username: 'x', code: null },
    ]);

    await expect(
      handler.main(buildCaller(), buildPayload({ email: 'taken@example.com' })),
    ).rejects.toMatchObject({
      response: { translation: 'iam.error.uniqueEmail' },
    });
  });

  it('skips uniqueness check when email matches existing (no change)', async () => {
    await handler.main(buildCaller(), buildPayload({ email: 'old@example.com' }));

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

  it('emits iam.account.updated on success', async () => {
    await handler.main(buildCaller(), buildPayload({ email: 'new@example.com' }));

    expect(eventEmitter.emit).toHaveBeenCalledWith('iam.account.updated', expect.any(Object));
  });
});
