import type { Request, Response } from 'express';
import type { Logger } from 'nestjs-pino';
import type { AuditWriter } from '../audit/audit.service';
import { PRE_AUTH_COOKIE_NAME } from '../auth/auth.cookie';
import { AuthCodeFlowException } from '../auth/auth.errors';
import type { AuthService, PreAuthPayload } from '../auth/auth.service';
import type { EntraConfig } from '../auth/entra-config.token';
import type { SessionEstablisher } from '../auth/session-establisher.service';
import { AdminAuthController } from './admin-auth.controller';

const ENTRA: EntraConfig = {
  instanceUrl: 'https://login.microsoftonline.com/',
  tenantId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
  clientId: '11111111-2222-3333-4444-555555555555',
  clientSecret: 's3cret',
  redirectUri: 'http://localhost:3000/api/auth/callback',
  postLogoutRedirectUri: 'http://localhost:4200/',
  adminRedirectUri: 'http://localhost:3000/api/admin/auth/callback',
  adminPostLogoutRedirectUri: 'http://localhost:4300/',
  authority: 'https://login.microsoftonline.com/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
};

const USER = {
  oid: 'admin-oid',
  tid: ENTRA.tenantId,
  username: 'admin@apf.example',
  displayName: 'Admin Smith',
  amr: ['pwd', 'mfa'],
  roles: ['admin'],
};

const PRE_AUTH: PreAuthPayload = {
  state: 'state-nonce',
  codeVerifier: 'verifier-secret',
  createdAt: 1_000,
};

const ADMIN_LOGOUT_URL = `${ENTRA.authority}/oauth2/v2.0/logout?post_logout_redirect_uri=${encodeURIComponent(ENTRA.adminPostLogoutRedirectUri)}`;

function makeResStub() {
  const res = {
    cookie: jest.fn(),
    clearCookie: jest.fn(),
    redirect: jest.fn(),
    status: jest.fn(),
    json: jest.fn(),
  };
  res.cookie.mockReturnValue(res);
  res.clearCookie.mockReturnValue(res);
  res.redirect.mockReturnValue(res);
  res.status.mockReturnValue(res);
  res.json.mockReturnValue(res);
  return res as unknown as Response & typeof res;
}

function makeReqStub(opts?: {
  signedCookies?: Record<string, unknown>;
  sessionUser?: typeof USER;
}): Request {
  return {
    signedCookies: opts?.signedCookies ?? {},
    session: opts?.sessionUser !== undefined ? { user: opts.sessionUser } : {},
    sessionID: 'sid-admin',
  } as unknown as Request;
}

function makeFixture(opts?: { completeAuthCodeFlow?: jest.Mock }) {
  const beginAuthCodeFlow = jest.fn().mockResolvedValue({
    authUrl: 'https://entra.example/authorize?state=state-nonce',
    preAuthPayload: PRE_AUTH,
  });
  const completeAuthCodeFlow = opts?.completeAuthCodeFlow ?? jest.fn().mockResolvedValue(USER);
  const buildLogoutUrl = jest.fn().mockReturnValue(ADMIN_LOGOUT_URL);
  const authService = {
    beginAuthCodeFlow,
    completeAuthCodeFlow,
    buildLogoutUrl,
  } as unknown as AuthService;
  const audit = {
    signIn: jest.fn().mockResolvedValue(undefined),
    signInFailed: jest.fn().mockResolvedValue(undefined),
    signOut: jest.fn().mockResolvedValue(undefined),
  };
  const establisher = {
    establish: jest.fn().mockResolvedValue(undefined),
    destroy: jest.fn().mockResolvedValue(undefined),
  };
  const logger = { log: jest.fn(), warn: jest.fn(), error: jest.fn() };
  return {
    controller: new AdminAuthController(
      authService,
      logger as unknown as Logger,
      ENTRA,
      audit as unknown as AuditWriter,
      establisher as unknown as SessionEstablisher,
    ),
    beginAuthCodeFlow,
    completeAuthCodeFlow,
    buildLogoutUrl,
    audit,
    establisher,
    logger,
  };
}

describe('AdminAuthController.login', () => {
  it('passes the admin redirect URI to beginAuthCodeFlow', async () => {
    const { controller, beginAuthCodeFlow } = makeFixture();
    await controller.login(makeResStub());
    expect(beginAuthCodeFlow).toHaveBeenCalledWith(ENTRA.adminRedirectUri);
  });

  it('writes the pre-auth cookie and 302s to the Entra auth URL', async () => {
    const { controller } = makeFixture();
    const res = makeResStub();
    await controller.login(res);
    expect(res.cookie).toHaveBeenCalledWith(
      PRE_AUTH_COOKIE_NAME,
      expect.any(String),
      expect.objectContaining({ signed: true, httpOnly: true }),
    );
    expect(res.redirect).toHaveBeenCalledWith(302, expect.stringContaining('entra.example'));
  });
});

describe('AdminAuthController.callback', () => {
  it('passes the admin redirect URI to completeAuthCodeFlow and establishes a surface=admin session', async () => {
    const { controller, completeAuthCodeFlow, establisher } = makeFixture();
    const res = makeResStub();
    const req = makeReqStub({
      signedCookies: { [PRE_AUTH_COOKIE_NAME]: JSON.stringify(PRE_AUTH) },
    });

    await controller.callback(req, res, 'auth-code', PRE_AUTH.state);

    expect(completeAuthCodeFlow).toHaveBeenCalledWith(
      'auth-code',
      PRE_AUTH.state,
      PRE_AUTH,
      ENTRA.adminRedirectUri,
    );
    expect(establisher.establish).toHaveBeenCalledWith({
      user: USER,
      req,
      res,
      surface: 'admin',
    });
    expect(res.redirect).toHaveBeenCalledWith(302, ENTRA.adminPostLogoutRedirectUri);
  });

  it('redirects with ?auth_error=token-exchange-failed when Entra returns error', async () => {
    const { controller } = makeFixture();
    const res = makeResStub();
    const req = makeReqStub({
      signedCookies: { [PRE_AUTH_COOKIE_NAME]: JSON.stringify(PRE_AUTH) },
    });

    await controller.callback(req, res, undefined, undefined, 'access_denied', 'consent declined');

    expect(res.redirect).toHaveBeenCalledWith(
      302,
      expect.stringContaining('auth_error=token-exchange-failed'),
    );
    // Error redirect lands on the admin post-logout target, not the user-portal one.
    expect(res.redirect).toHaveBeenCalledWith(
      302,
      expect.stringContaining(ENTRA.adminPostLogoutRedirectUri),
    );
  });

  it('redirects with ?auth_error=flow-expired when the pre-auth cookie is missing', async () => {
    const { controller, audit } = makeFixture();
    const res = makeResStub();
    const req = makeReqStub({ signedCookies: {} });

    await controller.callback(req, res, 'auth-code', PRE_AUTH.state);

    expect(res.redirect).toHaveBeenCalledWith(
      302,
      expect.stringContaining('auth_error=flow-expired'),
    );
    expect(audit.signInFailed).toHaveBeenCalledWith(
      expect.objectContaining({
        failureKind: 'no-pre-auth-cookie',
        payload: expect.objectContaining({ surface: 'admin' }),
      }),
    );
  });

  it('redirects with the typed error from AuthCodeFlowException', async () => {
    const completeAuthCodeFlow = jest
      .fn()
      .mockRejectedValue(new AuthCodeFlowException({ kind: 'state-mismatch' }));
    const { controller, audit } = makeFixture({ completeAuthCodeFlow });
    const res = makeResStub();
    const req = makeReqStub({
      signedCookies: { [PRE_AUTH_COOKIE_NAME]: JSON.stringify(PRE_AUTH) },
    });

    await controller.callback(req, res, 'auth-code', PRE_AUTH.state);

    expect(res.redirect).toHaveBeenCalledWith(
      302,
      expect.stringContaining('auth_error=state-mismatch'),
    );
    expect(audit.signInFailed).toHaveBeenCalledWith(
      expect.objectContaining({
        failureKind: 'state-mismatch',
        payload: expect.objectContaining({ surface: 'admin' }),
      }),
    );
  });
});

describe('AdminAuthController.me', () => {
  it('returns the public payload with roles when the admin session is populated', () => {
    const { controller } = makeFixture();
    const res = makeResStub();
    const req = makeReqStub({ sessionUser: USER });
    controller.me(req, res);
    expect(res.json).toHaveBeenCalledWith({
      oid: USER.oid,
      tid: USER.tid,
      username: USER.username,
      displayName: USER.displayName,
      roles: USER.roles,
    });
  });

  it('throws UnauthorizedException when no user is on the admin session', () => {
    const { controller } = makeFixture();
    const res = makeResStub();
    const req = makeReqStub();
    expect(() => controller.me(req, res)).toThrow(/Unauthenticated/);
  });
});

describe('AdminAuthController.logout', () => {
  it('destroys the admin session, clears the admin cookie + CSRF cookie, redirects to Entra logout', async () => {
    const { controller, establisher, buildLogoutUrl } = makeFixture();
    const res = makeResStub();
    const req = makeReqStub({ sessionUser: USER });

    await controller.logout(req, res);

    expect(establisher.destroy).toHaveBeenCalledWith({ actor: USER, req });
    expect(buildLogoutUrl).toHaveBeenCalledWith(ENTRA.adminPostLogoutRedirectUri);
    expect(res.clearCookie).toHaveBeenCalledWith('portal_admin_session', { path: '/' });
    expect(res.clearCookie).toHaveBeenCalledWith('portal_csrf', { path: '/' });
    expect(res.redirect).toHaveBeenCalledWith(302, ADMIN_LOGOUT_URL);
  });

  it('uses the __Host- prefixed admin cookie name in production', async () => {
    const originalNodeEnv = process.env['NODE_ENV'];
    try {
      process.env['NODE_ENV'] = 'production';
      const { controller } = makeFixture();
      const res = makeResStub();
      const req = makeReqStub({ sessionUser: USER });
      await controller.logout(req, res);
      expect(res.clearCookie).toHaveBeenCalledWith('__Host-portal_admin_session', {
        path: '/',
      });
    } finally {
      if (originalNodeEnv === undefined) {
        delete process.env['NODE_ENV'];
      } else {
        process.env['NODE_ENV'] = originalNodeEnv;
      }
    }
  });

  it('still clears cookies and redirects for an anonymous admin sign-out', async () => {
    const { controller, establisher } = makeFixture();
    const res = makeResStub();
    const req = makeReqStub();
    await controller.logout(req, res);
    expect(establisher.destroy).toHaveBeenCalledWith({ actor: undefined, req });
    expect(res.clearCookie).toHaveBeenCalledWith('portal_admin_session', { path: '/' });
    expect(res.redirect).toHaveBeenCalled();
  });
});
