import { describe, it, expect, vi, afterEach } from 'vitest';

// Mock passport-jwt before importing JwtStrategy
vi.mock('passport-jwt', () => {
  return {
    Strategy: class MockStrategy {
      constructor(options: any) {
        (this as any)._options = options;
      }
    },
    ExtractJwt: {
      fromAuthHeaderAsBearerToken: () => vi.fn(),
    },
  };
});

vi.mock('@nestjs/passport', () => {
  return {
    PassportStrategy: (StrategyClass: any) => {
      return class extends StrategyClass {};
    },
  };
});

// Stub shared module imports so tests don't have to wire real Prisma/Redis.
vi.mock('@modules/shared', () => ({
  PrismaService: class {},
  RedisService: class {},
}));

type PrismaStub = { user: { findUnique: ReturnType<typeof vi.fn> } };
type RedisStub = {
  isAvailable: ReturnType<typeof vi.fn>;
  get: ReturnType<typeof vi.fn>;
  set: ReturnType<typeof vi.fn>;
};

function makePrisma(user: { isActive: boolean; deletedAt: Date | null } | null): PrismaStub {
  return {
    user: {
      findUnique: vi.fn().mockResolvedValue(user),
    },
  };
}

function makeRedis(options: { available?: boolean; cached?: string | null } = {}): RedisStub {
  const { available = true, cached = null } = options;
  return {
    isAvailable: vi.fn().mockReturnValue(available),
    get: vi.fn().mockResolvedValue(cached),
    set: vi.fn().mockResolvedValue(undefined),
  };
}

const ACTIVE_USER = { isActive: true, deletedAt: null };
const BANNED_USER = { isActive: false, deletedAt: null };
const DELETED_USER = { isActive: true, deletedAt: new Date('2026-01-01T00:00:00Z') };

describe('JwtStrategy', () => {
  afterEach(() => {
    vi.unstubAllEnvs();
    vi.resetModules();
    vi.clearAllMocks();
  });

  it('throws if JWT_SECRET is missing', async () => {
    vi.stubEnv('JWT_SECRET', '');
    await expect(async () => {
      const { JwtStrategy } = await import('../strategies/jwt.strategy');
      new JwtStrategy(makePrisma(ACTIVE_USER) as never, makeRedis() as never);
    }).rejects.toThrow('JWT_SECRET environment variable is required');
  });

  it('creates strategy when JWT_SECRET is set', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const strategy = new JwtStrategy(makePrisma(ACTIVE_USER) as never, makeRedis() as never);
    expect(strategy).toBeDefined();
  });

  it('validate returns the payload when user is active and not deleted', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const strategy = new JwtStrategy(makePrisma(ACTIVE_USER) as never, makeRedis() as never);

    const payload = { sub: 'user-1', phone: '+84912345678', role: 'BUYER', iat: 12345, exp: 99999 };
    const result = await strategy.validate(payload);

    expect(result).toEqual({ sub: 'user-1', phone: '+84912345678', role: 'BUYER' });
  });

  it('validate strips extra fields from payload', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const strategy = new JwtStrategy(makePrisma(ACTIVE_USER) as never, makeRedis() as never);

    const payload = {
      sub: 'user-2',
      phone: '+84987654321',
      role: 'ADMIN',
      iat: 12345,
      exp: 99999,
      extra: 'data',
    } as any;
    const result = await strategy.validate(payload);

    expect(result).toEqual({ sub: 'user-2', phone: '+84987654321', role: 'ADMIN' });
    expect(result).not.toHaveProperty('extra');
    expect(result).not.toHaveProperty('iat');
  });

  it('rejects banned user (isActive=false) with 401', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const prisma = makePrisma(BANNED_USER);
    const strategy = new JwtStrategy(prisma as never, makeRedis() as never);

    await expect(
      strategy.validate({ sub: 'banned-1', phone: '+84911111111', role: 'BUYER' }),
    ).rejects.toMatchObject({ status: 401 });
  });

  it('rejects soft-deleted user (deletedAt !== null) with 401', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const strategy = new JwtStrategy(makePrisma(DELETED_USER) as never, makeRedis() as never);

    await expect(
      strategy.validate({ sub: 'deleted-1', phone: '+84922222222', role: 'BUYER' }),
    ).rejects.toMatchObject({ status: 401 });
  });

  it('rejects when user does not exist in DB', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const strategy = new JwtStrategy(makePrisma(null) as never, makeRedis() as never);

    await expect(
      strategy.validate({ sub: 'ghost-1', phone: '+84933333333', role: 'BUYER' }),
    ).rejects.toMatchObject({ status: 401 });
  });

  it('serves user status from Redis cache when present (no DB hit)', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const prisma = makePrisma(ACTIVE_USER);
    const redis = makeRedis({
      available: true,
      cached: JSON.stringify({ isActive: true, deletedAt: null }),
    });
    const strategy = new JwtStrategy(prisma as never, redis as never);

    const result = await strategy.validate({ sub: 'user-cached', phone: '+84900000001', role: 'BUYER' });
    expect(result.sub).toBe('user-cached');
    expect(prisma.user.findUnique).not.toHaveBeenCalled();
    expect(redis.get).toHaveBeenCalled();
    expect(redis.set).not.toHaveBeenCalled();
  });

  it('populates Redis cache with 60s TTL after DB lookup', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy, USER_STATUS_CACHE_TTL_SECONDS, USER_STATUS_CACHE_PREFIX } = await import(
      '../strategies/jwt.strategy'
    );
    const prisma = makePrisma(ACTIVE_USER);
    const redis = makeRedis({ available: true, cached: null });
    const strategy = new JwtStrategy(prisma as never, redis as never);

    await strategy.validate({ sub: 'user-miss', phone: '+84900000002', role: 'BUYER' });

    expect(prisma.user.findUnique).toHaveBeenCalledTimes(1);
    expect(redis.set).toHaveBeenCalledTimes(1);
    const [key, value, ttl] = redis.set.mock.calls[0];
    expect(key).toBe(`${USER_STATUS_CACHE_PREFIX}:user-miss`);
    expect(JSON.parse(value)).toEqual({ isActive: true, deletedAt: null });
    expect(ttl).toBe(USER_STATUS_CACHE_TTL_SECONDS);
    expect(USER_STATUS_CACHE_TTL_SECONDS).toBe(60);
  });

  it('falls back to DB when Redis is unavailable', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const prisma = makePrisma(ACTIVE_USER);
    const redis = makeRedis({ available: false });
    const strategy = new JwtStrategy(prisma as never, redis as never);

    const result = await strategy.validate({ sub: 'user-rdown', phone: '+84900000003', role: 'BUYER' });
    expect(result.sub).toBe('user-rdown');
    expect(redis.get).not.toHaveBeenCalled();
    expect(redis.set).not.toHaveBeenCalled();
    expect(prisma.user.findUnique).toHaveBeenCalledTimes(1);
  });

  it('falls back to DB when Redis read throws', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const prisma = makePrisma(ACTIVE_USER);
    const redis = makeRedis({ available: true });
    redis.get.mockRejectedValueOnce(new Error('boom'));
    const strategy = new JwtStrategy(prisma as never, redis as never);

    const result = await strategy.validate({ sub: 'user-rerr', phone: '+84900000004', role: 'BUYER' });
    expect(result.sub).toBe('user-rerr');
    expect(prisma.user.findUnique).toHaveBeenCalledTimes(1);
  });

  it('still rejects banned user when served from Redis cache', async () => {
    vi.stubEnv('JWT_SECRET', 'test-secret-key');
    const { JwtStrategy } = await import('../strategies/jwt.strategy');
    const redis = makeRedis({
      available: true,
      cached: JSON.stringify({ isActive: false, deletedAt: null }),
    });
    const strategy = new JwtStrategy(makePrisma(null) as never, redis as never);

    await expect(
      strategy.validate({ sub: 'banned-cached', phone: '+84900000005', role: 'BUYER' }),
    ).rejects.toMatchObject({ status: 401 });
  });
});
