/**
 * Unit test for AuthenticationJwtGuard.canActivate — none-mode shortcircuit.
 *
 * Note: testing @Auth-decorated classes via ts-jest pulls in ESM modules (cache-manager,
 * jwks-rsa) that are not transpiled and break the test runner. This test exercises the
 * guard CLASS DIRECTLY with a faked ExecutionContext, per the known ts-jest limitation
 * documented in catalyst-backend-testing SKILL.md.
 */
import { Api } from '@aurorajs.dev/core-back';
import { ExecutionContext } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Test } from '@nestjs/testing';
import * as anonymousAccountModule from '../../domain/anonymous-account';
import { AuthenticationJwtGuard } from './authentication-jwt.guard';

// ---------------------------------------------------------------------------
// Context factory helpers
// ---------------------------------------------------------------------------

function makeHttpContext(userContainer: { user?: unknown }): ExecutionContext {
  return {
    getType: () => 'http',
    switchToHttp: () => ({
      getRequest: () => userContainer,
    }),
    getClass: () => ({ name: 'TestController' }),
    getHandler: () => ({ name: 'testHandler' }),
    getArgs: () => [],
    getArgByIndex: () => undefined,
    switchToRpc: () => ({}),
    switchToWs: () => ({}),
  } as unknown as ExecutionContext;
}

function makeGraphqlContext(userContainer: { user?: unknown }): ExecutionContext {
  return {
    getType: () => 'graphql',
    switchToHttp: () => ({
      getRequest: () => ({}),
    }),
    getClass: () => ({ name: 'TestResolver' }),
    getHandler: () => ({ name: 'testQuery' }),
    getArgs: () => [null, null, { req: userContainer }, null],
    getArgByIndex: (index: number) => {
      const args = [null, null, { req: userContainer }, null];
      return args[index];
    },
    switchToRpc: () => ({}),
    switchToWs: () => ({}),
  } as unknown as ExecutionContext;
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('AuthenticationJwtGuard', () => {
  let guard: AuthenticationJwtGuard;
  let configService: { get: jest.Mock };

  beforeEach(async () => {
    configService = { get: jest.fn() };
    const module = await Test.createTestingModule({
      providers: [AuthenticationJwtGuard, { provide: ConfigService, useValue: configService }],
    }).compile();

    guard = module.get(AuthenticationJwtGuard);
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  describe('mode=none — shortcircuit path', () => {
    beforeEach(() => {
      configService.get.mockImplementation((key: string) => {
        if (key === 'OAUTH_STRATEGY') return 'none';
        return undefined;
      });
    });

    it('returns true for an HTTP context without calling super.canActivate', async () => {
      const userContainer: { user?: unknown } = {};
      const ctx = makeHttpContext(userContainer);

      const superSpy = jest
        .spyOn(Object.getPrototypeOf(Object.getPrototypeOf(guard)), 'canActivate')
        .mockResolvedValue(true);

      const result = await guard.canActivate(ctx);

      expect(result).toBe(true);
      expect(superSpy).not.toHaveBeenCalled();
    });

    it('injects an anonymous account into request.user (HTTP context)', async () => {
      const userContainer: { user?: unknown } = {};
      const ctx = makeHttpContext(userContainer);

      // Mock Api.getRequestFromExecutionContext to return our container
      jest.spyOn(Api, 'getRequestFromExecutionContext').mockReturnValue(userContainer);

      // Confirm createAnonymousAccount IS called in none mode (counterpart to the
      // local-provider negative assertion that it is NOT called).
      const createAnonymousSpy = jest.spyOn(anonymousAccountModule, 'createAnonymousAccount');

      await guard.canActivate(ctx);

      expect(createAnonymousSpy).toHaveBeenCalledTimes(1);
      expect(userContainer.user).toBeDefined();
      const account = userContainer.user as { username: string; dPermissions: { all: string[] } };
      expect(account.username).toBe('anonymous');
      expect(account.dPermissions.all).toEqual([]);
    });

    it('injects an anonymous account into request.user (GraphQL context)', async () => {
      const userContainer: { user?: unknown } = {};
      const ctx = makeGraphqlContext(userContainer);

      jest.spyOn(Api, 'getRequestFromExecutionContext').mockReturnValue(userContainer);

      const result = await guard.canActivate(ctx);

      expect(result).toBe(true);
      const account = userContainer.user as { username: string; dPermissions: { all: string[] } };
      expect(account.username).toBe('anonymous');
      expect(account.dPermissions.all).toEqual([]);
    });

    it('injects a FRESH account object on each call (not a shared reference)', async () => {
      const containerA: { user?: unknown } = {};
      const containerB: { user?: unknown } = {};
      const ctxA = makeHttpContext(containerA);
      const ctxB = makeHttpContext(containerB);

      jest
        .spyOn(Api, 'getRequestFromExecutionContext')
        .mockReturnValueOnce(containerA)
        .mockReturnValueOnce(containerB);

      await guard.canActivate(ctxA);
      await guard.canActivate(ctxB);

      expect(containerA.user).not.toBe(containerB.user);
    });
  });

  describe('mode=local-provider — delegate to super path', () => {
    beforeEach(() => {
      configService.get.mockImplementation((key: string) => {
        if (key === 'OAUTH_STRATEGY') return 'local-provider';
        return undefined;
      });
    });

    it('calls super.canActivate when mode is local-provider', async () => {
      const userContainer: { user?: unknown } = {};
      const ctx = makeHttpContext(userContainer);

      const superSpy = jest
        .spyOn(Object.getPrototypeOf(Object.getPrototypeOf(guard)), 'canActivate')
        .mockResolvedValue(true);

      await guard.canActivate(ctx);

      expect(superSpy).toHaveBeenCalledWith(ctx);
    });

    it('does NOT call createAnonymousAccount in local-provider mode', async () => {
      const userContainer: { user?: unknown } = {};
      const ctx = makeHttpContext(userContainer);

      // Spy on the module-level factory so we can assert the short-circuit
      // code path (none mode) never fires when JWT mode is active.
      const createAnonymousSpy = jest.spyOn(anonymousAccountModule, 'createAnonymousAccount');

      jest
        .spyOn(Object.getPrototypeOf(Object.getPrototypeOf(guard)), 'canActivate')
        .mockResolvedValue(true);

      await guard.canActivate(ctx);

      // The short-circuit path must NOT have run: createAnonymousAccount is
      // only called inside the `if (mode === 'none')` branch of the guard.
      // If this assertion fails, the guard mistakenly ran the none-mode path
      // for a JWT-based strategy — a security regression.
      expect(createAnonymousSpy).not.toHaveBeenCalled();

      // Confirm passport delegation happened (the super call is the JWT path).
      // This is already covered by the sibling 'calls super.canActivate' test;
      // repeating it here makes the negative guard self-contained.
      expect(userContainer.user).toBeUndefined();
    });
  });

  describe('handleRequest — unchanged rejection path', () => {
    it('throws UnauthorizedException when user is falsy', () => {
      expect(() => guard.handleRequest(null, null)).toThrow();
    });

    it('returns the user when valid', () => {
      const user = { username: 'test', dPermissions: { all: [] } };
      const result = guard.handleRequest(null, user);
      expect(result).toBe(user);
    });
  });
});
