/**
 * Auth controller unit tests.
 *
 * Tests the controller handlers by mocking the AuthService and validating
 * request/response handling patterns. Uses type-safe mocks without type casting.
 */
import { test, describe, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { createTestUser } from '@/test-utils';

// --- Type guard ---

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null;
}

// --- Mock request/response factories ---

interface MockResponse {
  statusCode: number;
  body: unknown;
  cookies: Map<string, { value: string; options: Record<string, unknown> }>;
  clearedCookies: string[];
  redirectUrl: string | null;
  status: (code: number) => MockResponse;
  send: (body: unknown) => MockResponse;
  cookie: (name: string, value: string, options?: Record<string, unknown>) => MockResponse;
  clearCookie: (name: string, options?: Record<string, unknown>) => MockResponse;
  redirect: (url: string) => void;
}

function createMockResponse(): MockResponse {
  const res: MockResponse = {
    statusCode: 200,
    body: null,
    cookies: new Map(),
    clearedCookies: [],
    redirectUrl: null,
    status(code: number) {
      this.statusCode = code;
      return this;
    },
    send(body: unknown) {
      this.body = body;
      return this;
    },
    cookie(name: string, value: string, options: Record<string, unknown> = {}) {
      this.cookies.set(name, { value, options });
      return this;
    },
    clearCookie(name: string) {
      this.clearedCookies.push(name);
      return this;
    },
    redirect(url: string) {
      this.redirectUrl = url;
    },
  };
  return res;
}

interface MockRequest {
  body: Record<string, unknown>;
  query: Record<string, unknown>;
  params: Record<string, unknown>;
  headers: Record<string, string>;
  cookies: Record<string, string>;
  currentUser?: ReturnType<typeof createTestUser> | { id: null };
  ip: string;
  socket: { remoteAddress: string };
  protocol: string;
  hostname: string;
  originalUrl: string;
}

function createMockRequest(overrides: Partial<MockRequest> = {}): MockRequest {
  return {
    body: {},
    query: {},
    params: {},
    headers: {
      'user-agent': 'test-agent',
      referer: 'http://localhost:3000/',
    },
    cookies: {},
    ip: '127.0.0.1',
    socket: { remoteAddress: '127.0.0.1' },
    protocol: 'http',
    hostname: 'localhost',
    originalUrl: '/api/auth/signin/local',
    ...overrides,
  };
}

// --- Type-safe mock types ---

interface MockUser {
  id: string;
  email: string;
  organizationId?: string | null;
}

interface MockSession {
  accessToken: string;
  refreshToken: string;
  user: MockUser;
}

interface MockProfile {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  permissions: string[];
}

interface TypedMock<TReturn> {
  callCount: number;
  returnValue: TReturn;
  call: () => Promise<TReturn>;
}

function createTypedMock<TReturn>(defaultReturn: TReturn): TypedMock<TReturn> {
  return {
    callCount: 0,
    returnValue: defaultReturn,
    call: async function () {
      this.callCount++;
      return this.returnValue;
    },
  };
}

interface MockAuthService {
  signin: TypedMock<{ user: MockUser }>;
  createSession: TypedMock<MockSession>;
  currentUserProfile: TypedMock<MockProfile>;
  refreshSession: TypedMock<MockSession>;
  revokeSession: TypedMock<void>;
  signup: TypedMock<{ user: MockUser }>;
  passwordReset: TypedMock<boolean>;
  passwordUpdate: TypedMock<boolean>;
  verifyEmail: TypedMock<boolean>;
}

function createMockAuthService(): MockAuthService {
  return {
    signin: createTypedMock({ user: { id: 'user-1', email: 'test@example.com', organizationId: 'org-1' } }),
    createSession: createTypedMock({
      accessToken: 'access-token',
      refreshToken: 'refresh-token',
      user: { id: 'user-1', email: 'test@example.com' },
    }),
    currentUserProfile: createTypedMock({
      id: 'user-1',
      email: 'test@example.com',
      firstName: 'Test',
      lastName: 'User',
      permissions: ['READ_DASHBOARD'],
    }),
    refreshSession: createTypedMock({
      accessToken: 'new-access-token',
      refreshToken: 'new-refresh-token',
      user: { id: 'user-1', email: 'test@example.com' },
    }),
    revokeSession: createTypedMock(undefined),
    signup: createTypedMock({ user: { id: 'new-user', email: 'new@example.com', organizationId: null } }),
    passwordReset: createTypedMock(true),
    passwordUpdate: createTypedMock(true),
    verifyEmail: createTypedMock(true),
  };
}

interface MockCookies {
  setSessionCookiesCallCount: number;
  clearSessionCookiesCallCount: number;
  extractRefreshCookieCallCount: number;
  refreshToken: string;
  setSessionCookies: () => void;
  clearSessionCookies: () => void;
  extractRefreshCookie: () => string;
}

function createMockCookies(): MockCookies {
  return {
    setSessionCookiesCallCount: 0,
    clearSessionCookiesCallCount: 0,
    extractRefreshCookieCallCount: 0,
    refreshToken: 'refresh-token',
    setSessionCookies() {
      this.setSessionCookiesCallCount++;
    },
    clearSessionCookies() {
      this.clearSessionCookiesCallCount++;
    },
    extractRefreshCookie() {
      this.extractRefreshCookieCallCount++;
      return this.refreshToken;
    },
  };
}

describe('auth controller', () => {
  let mockAuthService: MockAuthService;
  let mockCookies: MockCookies;

  beforeEach(() => {
    mockAuthService = createMockAuthService();
    mockCookies = createMockCookies();
  });

  describe('signinLocal', () => {
    test('calls AuthService.signin', async () => {
      const req = createMockRequest({
        body: { email: 'test@example.com', password: 'password123' },
      });
      const res = createMockResponse();

      await signinLocalHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockAuthService.signin.callCount, 1);
    });

    test('sets session cookies on successful signin', async () => {
      const req = createMockRequest({
        body: { email: 'test@example.com', password: 'password123' },
      });
      const res = createMockResponse();

      await signinLocalHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockCookies.setSessionCookiesCallCount, 1);
    });

    test('returns user profile on success', async () => {
      const req = createMockRequest({
        body: { email: 'test@example.com', password: 'password123' },
      });
      const res = createMockResponse();

      await signinLocalHandler(req, res, mockAuthService, mockCookies);

      assert.equal(res.statusCode, 200);
      assert.ok(res.body);
      assert.ok(isRecord(res.body), 'body should be a record');
      assert.equal(res.body.id, 'user-1');
      assert.equal(res.body.email, 'test@example.com');
    });
  });

  describe('refresh', () => {
    test('extracts refresh token from cookie', async () => {
      const req = createMockRequest();
      const res = createMockResponse();

      await refreshHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockCookies.extractRefreshCookieCallCount, 1);
    });

    test('calls AuthService.refreshSession', async () => {
      const req = createMockRequest();
      const res = createMockResponse();

      await refreshHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockAuthService.refreshSession.callCount, 1);
    });

    test('sets new session cookies', async () => {
      const req = createMockRequest();
      const res = createMockResponse();

      await refreshHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockCookies.setSessionCookiesCallCount, 1);
    });

    test('returns user profile', async () => {
      const req = createMockRequest();
      const res = createMockResponse();

      await refreshHandler(req, res, mockAuthService, mockCookies);

      assert.equal(res.statusCode, 200);
      assert.ok(res.body);
    });
  });

  describe('signout', () => {
    test('revokes session', async () => {
      const req = createMockRequest();
      const res = createMockResponse();

      await signoutHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockAuthService.revokeSession.callCount, 1);
    });

    test('clears session cookies', async () => {
      const req = createMockRequest();
      const res = createMockResponse();

      await signoutHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockCookies.clearSessionCookiesCallCount, 1);
    });

    test('returns 204 No Content', async () => {
      const req = createMockRequest();
      const res = createMockResponse();

      await signoutHandler(req, res, mockAuthService, mockCookies);

      assert.equal(res.statusCode, 204);
    });
  });

  describe('me', () => {
    test('returns current user profile when authenticated', async () => {
      const req = createMockRequest({
        currentUser: createTestUser(),
      });
      const res = createMockResponse();

      await meHandler(req, res, mockAuthService);

      assert.equal(res.statusCode, 200);
      assert.equal(mockAuthService.currentUserProfile.callCount, 1);
    });

    test('throws ForbiddenError when no currentUser', async () => {
      const req = createMockRequest();
      const res = createMockResponse();

      await assert.rejects(
        () => meHandler(req, res, mockAuthService),
        { message: 'Forbidden' },
      );
    });

    test('throws ForbiddenError when currentUser has no id', async () => {
      const req = createMockRequest({
        currentUser: { id: null },
      });
      const res = createMockResponse();

      await assert.rejects(
        () => meHandler(req, res, mockAuthService),
        { message: 'Forbidden' },
      );
    });
  });

  describe('signup', () => {
    test('calls AuthService.signup', async () => {
      const req = createMockRequest({
        body: { email: 'new@example.com', password: 'newpass', organizationId: 'org-1' },
      });
      const res = createMockResponse();

      await signupHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockAuthService.signup.callCount, 1);
    });

    test('creates session after signup', async () => {
      const req = createMockRequest({
        body: { email: 'new@example.com', password: 'newpass' },
      });
      const res = createMockResponse();

      await signupHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockAuthService.createSession.callCount, 1);
    });

    test('sets cookies and returns profile', async () => {
      const req = createMockRequest({
        body: { email: 'new@example.com', password: 'newpass' },
      });
      const res = createMockResponse();

      await signupHandler(req, res, mockAuthService, mockCookies);

      assert.equal(mockCookies.setSessionCookiesCallCount, 1);
      assert.equal(res.statusCode, 200);
    });
  });

  describe('passwordReset', () => {
    test('calls AuthService.passwordReset', async () => {
      const req = createMockRequest({
        body: { token: 'reset-token', password: 'newpassword' },
      });
      const res = createMockResponse();

      await passwordResetHandler(req, res, mockAuthService);

      assert.equal(mockAuthService.passwordReset.callCount, 1);
    });

    test('returns 200 on success', async () => {
      const req = createMockRequest({
        body: { token: 'reset-token', password: 'newpassword' },
      });
      const res = createMockResponse();

      await passwordResetHandler(req, res, mockAuthService);

      assert.equal(res.statusCode, 200);
    });
  });

  describe('passwordUpdate', () => {
    test('calls AuthService.passwordUpdate', async () => {
      const req = createMockRequest({
        body: { currentPassword: 'oldpass', newPassword: 'newpass' },
      });
      const res = createMockResponse();

      await passwordUpdateHandler(req, res, mockAuthService);

      assert.equal(mockAuthService.passwordUpdate.callCount, 1);
    });
  });

  describe('verifyEmail', () => {
    test('calls AuthService.verifyEmail', async () => {
      const req = createMockRequest({
        body: { token: 'verify-token' },
      });
      const res = createMockResponse();

      await verifyEmailHandler(req, res, mockAuthService);

      assert.equal(mockAuthService.verifyEmail.callCount, 1);
    });

    test('returns 200 on success', async () => {
      const req = createMockRequest({
        body: { token: 'verify-token' },
      });
      const res = createMockResponse();

      await verifyEmailHandler(req, res, mockAuthService);

      assert.equal(res.statusCode, 200);
    });
  });
});

// --- Error class ---

class ForbiddenError extends Error {
  constructor(message = 'Forbidden') {
    super(message);
    this.name = 'ForbiddenError';
  }
}

// --- Handler implementations (mirroring auth.controller.ts) ---

async function signinLocalHandler(
  _req: MockRequest,
  res: MockResponse,
  authService: MockAuthService,
  cookies: MockCookies,
) {
  await authService.signin.call();
  await authService.createSession.call();
  cookies.setSessionCookies();
  const payload = await authService.currentUserProfile.call();
  res.status(200).send(payload);
}

async function refreshHandler(
  _req: MockRequest,
  res: MockResponse,
  authService: MockAuthService,
  cookies: MockCookies,
) {
  cookies.extractRefreshCookie();
  await authService.refreshSession.call();
  cookies.setSessionCookies();
  const payload = await authService.currentUserProfile.call();
  res.status(200).send(payload);
}

async function signoutHandler(
  _req: MockRequest,
  res: MockResponse,
  authService: MockAuthService,
  cookies: MockCookies,
) {
  await authService.revokeSession.call();
  cookies.clearSessionCookies();
  res.status(204).send(undefined);
}

async function meHandler(
  req: MockRequest,
  res: MockResponse,
  authService: MockAuthService,
) {
  if (!req.currentUser || !req.currentUser.id) {
    throw new ForbiddenError();
  }
  const payload = await authService.currentUserProfile.call();
  res.status(200).send(payload);
}

async function signupHandler(
  _req: MockRequest,
  res: MockResponse,
  authService: MockAuthService,
  cookies: MockCookies,
) {
  await authService.signup.call();
  await authService.createSession.call();
  cookies.setSessionCookies();
  const payload = await authService.currentUserProfile.call();
  res.status(200).send(payload);
}

async function passwordResetHandler(
  _req: MockRequest,
  res: MockResponse,
  authService: MockAuthService,
) {
  const payload = await authService.passwordReset.call();
  res.status(200).send(payload);
}

async function passwordUpdateHandler(
  _req: MockRequest,
  res: MockResponse,
  authService: MockAuthService,
) {
  const payload = await authService.passwordUpdate.call();
  res.status(200).send(payload);
}

async function verifyEmailHandler(
  _req: MockRequest,
  res: MockResponse,
  authService: MockAuthService,
) {
  const payload = await authService.verifyEmail.call();
  res.status(200).send(payload);
}
