/**
 * Shared Jest setup.
 *
 * Jest loads this module once through setupFilesAfterEnv. Test files under
 * src/__tests__ re-export from it so every production module and every test
 * observes the same Prisma and Redis mock instances.
 */

import { afterEach, beforeAll, beforeEach, jest } from '@jest/globals';
import { Buffer } from 'node:buffer';
import process from 'node:process';
import { mockDeep, mockReset } from 'jest-mock-extended';
import type { DeepMockProxy } from 'jest-mock-extended';
import type { PrismaClient } from '@prisma/client';

const MOCK_REDIS_SCRIPT_SHA = 'mock-sha-1234567890abcdef1234567890abcdef12345678';
const MOCK_REDIS_RATE_LIMIT_RESULT = [1, 60];

const mockPrismaClient: DeepMockProxy<PrismaClient> = mockDeep<PrismaClient>();

/** The single Prisma client used by all tests and mocked production modules. */
export const prismaMock = mockPrismaClient;

jest.mock('@prisma/client', () => {
  const prismaClient = jest.requireActual<typeof import('@prisma/client')>('@prisma/client');

  return {
    ...prismaClient,
    // Keep the constructor as a plain function: Jest's resetMocks option must not
    // erase how newly loaded production modules obtain the shared client.
    PrismaClient: function PrismaClientMock() {
      return mockPrismaClient;
    },
  };
});

type RedisMockFunction = (...args: unknown[]) => Promise<unknown>;
const redisMock = () => jest.fn<RedisMockFunction>();

/** The single Redis client used by all tests and mocked production modules. */
const mockRedisClient = {
  status: 'ready',
  get: redisMock(),
  set: redisMock(),
  setex: redisMock(),
  del: redisMock(),
  expire: redisMock(),
  ttl: redisMock(),
  incr: redisMock(),
  decr: redisMock(),
  exists: redisMock(),
  keys: redisMock(),
  scan: redisMock(),
  hget: redisMock(),
  hset: redisMock(),
  hdel: redisMock(),
  hgetall: redisMock(),
  lpush: redisMock(),
  rpush: redisMock(),
  lpop: redisMock(),
  rpop: redisMock(),
  lrange: redisMock(),
  zadd: redisMock(),
  zrange: redisMock(),
  zrangebyscore: redisMock(),
  zrem: redisMock(),
  sadd: redisMock(),
  srem: redisMock(),
  smembers: redisMock(),
  publish: redisMock(),
  subscribe: redisMock(),
  unsubscribe: redisMock(),
  connect: redisMock(),
  ping: redisMock(),
  quit: redisMock(),
  disconnect: redisMock(),
  on: redisMock(),
  once: redisMock(),
  off: redisMock(),
  emit: redisMock(),
  call: redisMock(),
  sendCommand: redisMock(),
  evalsha: redisMock(),
  eval: redisMock(),
  script: redisMock(),
};

export { mockRedisClient };

jest.mock('ioredis', () => function RedisMock() {
  return mockRedisClient;
});

function restoreRedisDefaults(): void {
  mockRedisClient.status = 'ready';
  mockRedisClient.connect.mockResolvedValue(undefined);
  mockRedisClient.ping.mockResolvedValue('PONG');
  mockRedisClient.quit.mockResolvedValue('OK');
  mockRedisClient.disconnect.mockResolvedValue(undefined);
  mockRedisClient.publish.mockResolvedValue(1);
  mockRedisClient.subscribe.mockResolvedValue(1);
  mockRedisClient.unsubscribe.mockResolvedValue(0);
  mockRedisClient.sadd.mockResolvedValue(1);
  mockRedisClient.srem.mockResolvedValue(1);
  mockRedisClient.smembers.mockResolvedValue([]);
  mockRedisClient.zrangebyscore.mockResolvedValue([]);
  // rate-limit-redis reads [totalHits, resetSeconds] from its Lua script and
  // rejects a zero hit count, so the store must report at least this request.
  mockRedisClient.call.mockImplementation((...args: unknown[]) => {
    if (String(args[0]).toUpperCase() === 'SCRIPT') {
      return Promise.resolve(MOCK_REDIS_SCRIPT_SHA);
    }
    return Promise.resolve(MOCK_REDIS_RATE_LIMIT_RESULT);
  });
  mockRedisClient.sendCommand.mockResolvedValue(MOCK_REDIS_SCRIPT_SHA);
  mockRedisClient.evalsha.mockResolvedValue(MOCK_REDIS_RATE_LIMIT_RESULT);
  mockRedisClient.eval.mockResolvedValue(MOCK_REDIS_RATE_LIMIT_RESULT);
  mockRedisClient.script.mockResolvedValue(MOCK_REDIS_SCRIPT_SHA);
}

restoreRedisDefaults();

/**
 * Test environment values are intentionally non-production and satisfy the
 * runtime validators exercised while modules are imported.
 */
process.env.NODE_ENV = 'test';
process.env.PORT = '3001';
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test';
process.env.REDIS_HOST = 'localhost';
process.env.REDIS_PORT = '6379';
process.env.REDIS_DB = '1';
process.env.JWT_ACCESS_SECRET = 'test-access-secret-32-chars-long-for-testing';
process.env.JWT_REFRESH_SECRET = 'test-refresh-secret-32-chars-long-for-testing';
process.env.JWT_ADMIN_SECRET = 'test-admin-secret-32-chars-long-for-testing--';
process.env.JWT_ACCESS_EXPIRY = '15m';
process.env.JWT_REFRESH_EXPIRY = '7d';
process.env.GEMINI_API_KEY = 'test-gemini-api-key';
process.env.PAYPAL_CLIENT_ID = 'test-paypal-client-id';
process.env.PAYPAL_CLIENT_SECRET = 'test-paypal-client-secret';
process.env.PAYPAL_WEBHOOK_ID = 'test-paypal-webhook-id';
process.env.PAYPAL_MODE = 'sandbox';
process.env.PAYPAL_PLAN_STARTER_MONTHLY = 'P-STARTER-MONTHLY-TEST';
process.env.PAYPAL_PLAN_STARTER_ANNUAL = 'P-STARTER-ANNUAL-TEST';
process.env.PAYPAL_PLAN_PROFESSIONAL_MONTHLY = 'P-PRO-MONTHLY-TEST';
process.env.PAYPAL_PLAN_PROFESSIONAL_ANNUAL = 'P-PRO-ANNUAL-TEST';
process.env.PAYPAL_PLAN_ENTERPRISE_MONTHLY = 'P-ENT-MONTHLY-TEST';
process.env.PAYPAL_PLAN_ENTERPRISE_ANNUAL = 'P-ENT-ANNUAL-TEST';
process.env.FRONTEND_URL = 'https://test.translate.press.zone';
process.env.ADMIN_PANEL_URL = 'https://test-admin.translate.press.zone';
process.env.API_URL = 'https://test-api.translate.press.zone';
process.env.CORS_ALLOWED_ORIGINS = 'https://test.translate.press.zone,https://test-admin.translate.press.zone';
process.env.LOG_LEVEL = 'error';
process.env.CREDITS_STARTER = '100000';
process.env.CREDITS_PROFESSIONAL = '500000';
process.env.CREDITS_ENTERPRISE = '2000000';
process.env.RATE_LIMIT_STARTER = '60';
process.env.RATE_LIMIT_PROFESSIONAL = '120';
process.env.RATE_LIMIT_ENTERPRISE = '0';
process.env.MAX_SYNC_CHARS = '5000';
process.env.MAX_ASYNC_CHARS = '50000';

beforeEach(() => {
  mockReset(prismaMock);

  for (const value of Object.values(mockRedisClient)) {
    if (jest.isMockFunction(value)) {
      value.mockReset();
    }
  }

  restoreRedisDefaults();
});

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

beforeAll(() => {
  jest.setTimeout(10000);
});

/** Wait for a specific amount of time in asynchronous tests. */
export const wait = (ms: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, ms));

let dateSpy: { mockRestore(): void } | undefined;

/** Replace the global Date constructor with a deterministic date. */
export const mockTimestamp = (timestamp: string | number): void => {
  const date = new Date(timestamp);
  dateSpy?.mockRestore();
  dateSpy = jest.spyOn(globalThis, 'Date').mockImplementation(() => date as any);
};

/** Restore a Date constructor mock created by mockTimestamp. */
export const restoreDateMock = (): void => {
  dateSpy?.mockRestore();
  dateSpy = undefined;
};

/** Generate a deterministic UUID-shaped value for tests. */
export const mockUUID = (seed: number = 1): string =>
  `00000000-0000-4000-8000-${seed.toString().padStart(12, '0')}`;

/** Generate a syntactically valid mock API key. */
export const mockApiKey = (prefix: string = 'sk_test'): string =>
  `${prefix}_${Math.random().toString(36).substring(2, 15)}`;

/** Generate a simple unsigned-looking JWT value for middleware fixtures. */
export const mockJWT = (payload: Record<string, unknown>): string => {
  const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64');
  const body = Buffer.from(JSON.stringify(payload)).toString('base64');
  return `${header}.${body}.mock-signature`;
};
