import { OAuthClientGrantTypeEnum } from '@api/graphql';
import { BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common';
import { InvalidClientError, InvalidRequestError, UnauthorizedClientError } from '../../flows/domain/errors';
import { type OAuthFindApplicationPort } from './o-auth-authenticate-application.function';
import {
  oAuthResolveApplicationClient,
  oAuthResolveApplicationClientWithoutSecret,
} from './o-auth-resolve-application-client.function';

const APPLICATION_CODE = 'aurora';
const APPLICATION_SECRET = 'top-secret-12345';
const CLIENT_ID = 'cli-1111-1111-1111-111111111111';

const buildApplication = (overrides: Record<string, unknown> = {}) => ({
  id: 'app-1111-1111-1111-111111111111',
  code: APPLICATION_CODE,
  secret: APPLICATION_SECRET,
  clients: [{ id: CLIENT_ID, grantType: 'PASSWORD' }],
  ...overrides,
});

const buildFind = (impl: jest.Mock): OAuthFindApplicationPort => ({ main: impl });

const args = (overrides: Partial<Parameters<typeof oAuthResolveApplicationClient>[1]> = {}) => ({
  clientId: APPLICATION_CODE,
  clientSecret: APPLICATION_SECRET,
  grantType: OAuthClientGrantTypeEnum.PASSWORD,
  ...overrides,
});

describe('oAuthResolveApplicationClient', () => {
  it('resolves the single client matching the grant type', async () => {
    const findApplication = buildFind(jest.fn().mockResolvedValue(buildApplication()));

    const client = await oAuthResolveApplicationClient(findApplication, args());

    expect(client.id).toBe(CLIENT_ID);
  });

  it('queries the application by code with clients eager-loaded', async () => {
    const main = jest.fn().mockResolvedValue(buildApplication());
    await oAuthResolveApplicationClient(buildFind(main), args());

    expect(main).toHaveBeenCalledWith({
      where: { code: APPLICATION_CODE },
      include: [{ association: 'clients' }],
    });
  });

  it('throws InvalidClientError when the application is not found (repository throws)', async () => {
    const findApplication = buildFind(jest.fn().mockRejectedValue(new Error('NotFound')));

    await expect(oAuthResolveApplicationClient(findApplication, args())).rejects.toBeInstanceOf(
      InvalidClientError,
    );
  });

  it('throws InvalidClientError when the application secret mismatches', async () => {
    const findApplication = buildFind(
      jest.fn().mockResolvedValue(buildApplication({ secret: 'different' })),
    );

    await expect(oAuthResolveApplicationClient(findApplication, args())).rejects.toBeInstanceOf(
      InvalidClientError,
    );
  });

  it('throws UnauthorizedClientError when no client matches the grant type', async () => {
    const findApplication = buildFind(
      jest
        .fn()
        .mockResolvedValue(
          buildApplication({ clients: [{ id: 'x', grantType: 'REFRESH_TOKEN' }] }),
        ),
    );

    await expect(oAuthResolveApplicationClient(findApplication, args())).rejects.toBeInstanceOf(
      UnauthorizedClientError,
    );
  });

  it('throws UnauthorizedClientError when the application has no clients at all', async () => {
    const findApplication = buildFind(
      jest.fn().mockResolvedValue(buildApplication({ clients: [] })),
    );

    await expect(oAuthResolveApplicationClient(findApplication, args())).rejects.toBeInstanceOf(
      UnauthorizedClientError,
    );
  });

  it('throws InternalServerErrorException and logs when more than one client matches the grant', async () => {
    const errorSpy = jest.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
    const findApplication = buildFind(
      jest.fn().mockResolvedValue(
        buildApplication({
          clients: [
            { id: 'cli-a', grantType: 'PASSWORD' },
            { id: 'cli-b', grantType: 'PASSWORD' },
          ],
        }),
      ),
    );

    await expect(oAuthResolveApplicationClient(findApplication, args())).rejects.toBeInstanceOf(
      InternalServerErrorException,
    );
    expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('ambiguous configuration'));

    errorSpy.mockRestore();
  });

  it('selects the REFRESH_TOKEN client when that grant is requested', async () => {
    const findApplication = buildFind(
      jest.fn().mockResolvedValue(
        buildApplication({
          clients: [
            { id: 'cli-pwd', grantType: 'PASSWORD' },
            { id: 'cli-ref', grantType: 'REFRESH_TOKEN' },
          ],
        }),
      ),
    );

    const client = await oAuthResolveApplicationClient(
      findApplication,
      args({ grantType: OAuthClientGrantTypeEnum.REFRESH_TOKEN }),
    );

    expect(client.id).toBe('cli-ref');
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// oAuthResolveApplicationClientWithoutSecret
// RFC 6749 §4.1.1: the Authorization Request is a browser redirect — no secret.
// ──────────────────────────────────────────────────────────────────────────────

describe('oAuthResolveApplicationClientWithoutSecret', () => {
  const AUTH_CODE_CLIENT_ID = 'cli-auth-code-uuid';

  const buildAppWithAuthCodeClient = (overrides: Record<string, unknown> = {}) =>
    buildApplication({
      clients: [{ id: AUTH_CODE_CLIENT_ID, grantType: 'AUTHORIZATION_CODE' }],
      ...overrides,
    });

  const argsWithoutSecret = (
    overrides: Partial<Parameters<typeof oAuthResolveApplicationClientWithoutSecret>[1]> = {},
  ) => ({
    clientId: APPLICATION_CODE,
    grantType: OAuthClientGrantTypeEnum.AUTHORIZATION_CODE,
    ...overrides,
  });

  // ── RFC 6749 §4.1.1 REGRESSION GUARD ─────────────────────────────────────
  // Proves the bug: using oAuthResolveApplicationClient (with-secret) at /authorize
  // with a real non-empty secret always returns InvalidClientError because
  // oAuthSafeStringCompare(secret, '') fails for any real secret.
  it('resolves the AUTHORIZATION_CODE client even when the application has a NON-EMPTY secret (RFC 6749 §4.1.1)', async () => {
    const APPLICATION_WITH_REAL_SECRET = 'app-secret-123';
    const findApplication = buildFind(
      jest.fn().mockResolvedValue(
        buildAppWithAuthCodeClient({ secret: APPLICATION_WITH_REAL_SECRET }),
      ),
    );

    // This MUST succeed — no secret is required at /authorize.
    const client = await oAuthResolveApplicationClientWithoutSecret(
      findApplication,
      argsWithoutSecret(),
    );

    expect(client.id).toBe(AUTH_CODE_CLIENT_ID);
  });

  it('resolves the client matching AUTHORIZATION_CODE grant', async () => {
    const findApplication = buildFind(
      jest.fn().mockResolvedValue(buildAppWithAuthCodeClient()),
    );

    const client = await oAuthResolveApplicationClientWithoutSecret(
      findApplication,
      argsWithoutSecret(),
    );

    expect(client.id).toBe(AUTH_CODE_CLIENT_ID);
  });

  it('queries the application by code with clients eager-loaded (no secret sent)', async () => {
    const main = jest.fn().mockResolvedValue(buildAppWithAuthCodeClient());
    await oAuthResolveApplicationClientWithoutSecret(buildFind(main), argsWithoutSecret());

    expect(main).toHaveBeenCalledWith({
      where: { code: APPLICATION_CODE },
      include: [{ association: 'clients' }],
    });
  });

  it('propagates unexpected repository errors as-is (does NOT convert to InvalidClientError)', async () => {
    // Per Fix 3: only "not found" (null return) becomes InvalidClientError.
    // Transient DB faults (throws) propagate → 500, not invalid_client.
    const dbError = new Error('DB connection refused');
    const findApplication = buildFind(jest.fn().mockRejectedValue(dbError));

    await expect(
      oAuthResolveApplicationClientWithoutSecret(findApplication, argsWithoutSecret()),
    ).rejects.toBe(dbError);
  });

  // ── HTTP STATUS REGRESSION GUARD (spec §4.1 — authorize path) ───────────────
  // RFC 6749 §4.1.2.1 + spec requirement: an unknown client_id at /authorize
  // MUST be rejected with HTTP 400 WITHOUT redirecting. InvalidClientError (401)
  // is CORRECT for the /token endpoint (client authentication failure) but WRONG
  // here — the /authorize endpoint does not authenticate the client, it only
  // identifies it. An unknown client_id is a bad-request, not an auth failure.
  it('throws a 400-class error (NOT 401) when the application query returns null — spec §4.1 regression guard', async () => {
    const findApplication = buildFind(jest.fn().mockResolvedValue(null));

    const rejection = oAuthResolveApplicationClientWithoutSecret(
      findApplication,
      argsWithoutSecret(),
    );

    // Assert the concrete error type (InvalidRequestError extends BadRequestException → 400).
    await expect(rejection).rejects.toBeInstanceOf(InvalidRequestError);
    // Belt-and-suspenders: verify it is a BadRequestException (HTTP 400), NOT UnauthorizedException (401).
    await expect(rejection).rejects.toBeInstanceOf(BadRequestException);
  });

  it('throws UnauthorizedClientError when no client has AUTHORIZATION_CODE grant', async () => {
    const findApplication = buildFind(
      jest.fn().mockResolvedValue(
        buildApplication({ clients: [{ id: 'cli-x', grantType: 'PASSWORD' }] }),
      ),
    );

    await expect(
      oAuthResolveApplicationClientWithoutSecret(findApplication, argsWithoutSecret()),
    ).rejects.toBeInstanceOf(UnauthorizedClientError);
  });

  it('throws InternalServerErrorException when multiple AUTHORIZATION_CODE clients exist', async () => {
    const errorSpy = jest.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
    const findApplication = buildFind(
      jest.fn().mockResolvedValue(
        buildApplication({
          clients: [
            { id: 'cli-a', grantType: 'AUTHORIZATION_CODE' },
            { id: 'cli-b', grantType: 'AUTHORIZATION_CODE' },
          ],
        }),
      ),
    );

    await expect(
      oAuthResolveApplicationClientWithoutSecret(findApplication, argsWithoutSecret()),
    ).rejects.toBeInstanceOf(InternalServerErrorException);
    expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('ambiguous configuration'));

    errorSpy.mockRestore();
  });
});
