import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Logger } from '@nestjs/common';
import type { QueueHandle } from '@mosaicstack/queue';
import type { LogService } from '@mosaicstack/log';
import { SessionGCService } from './session-gc.service.js';

type MockRedis = {
  scan: ReturnType<typeof vi.fn>;
  del: ReturnType<typeof vi.fn>;
};

describe('SessionGCService', () => {
  let service: SessionGCService;
  let mockRedis: MockRedis;
  let mockLogService: { logs: { promoteToWarm: ReturnType<typeof vi.fn> } };

  /**
   * Helper: build a scan mock that returns all provided keys in a single
   * cursor iteration (cursor '0' in → ['0', keys] out).
   */
  function makeScanMock(keys: string[]): ReturnType<typeof vi.fn> {
    return vi.fn().mockResolvedValue(['0', keys]);
  }

  beforeEach(() => {
    mockRedis = {
      scan: makeScanMock([]),
      del: vi.fn().mockResolvedValue(0),
    };

    mockLogService = {
      logs: {
        promoteToWarm: vi.fn().mockResolvedValue(0),
      },
    };

    // Suppress logger output in tests
    vi.spyOn(Logger.prototype, 'log').mockImplementation(() => {});

    service = new SessionGCService(
      mockRedis as unknown as QueueHandle['redis'],
      mockLogService as unknown as LogService,
    );
  });

  it('collect() deletes Valkey keys for session', async () => {
    mockRedis.scan = makeScanMock(['mosaic:session:abc:system', 'mosaic:session:abc:foo']);
    const result = await service.collect('abc');
    expect(mockRedis.del).toHaveBeenCalledWith(
      'mosaic:session:abc:system',
      'mosaic:session:abc:foo',
    );
    expect(result.cleaned.valkeyKeys).toBe(2);
  });

  it('collect() with no keys returns empty cleaned valkeyKeys', async () => {
    mockRedis.scan = makeScanMock([]);
    const result = await service.collect('abc');
    expect(result.cleaned.valkeyKeys).toBeUndefined();
  });

  it('collect() returns sessionId in result', async () => {
    const result = await service.collect('test-session-id');
    expect(result.sessionId).toBe('test-session-id');
  });

  it('fullCollect() deletes all session keys', async () => {
    mockRedis.scan = makeScanMock(['mosaic:session:abc:system', 'mosaic:session:xyz:foo']);
    const result = await service.fullCollect();
    expect(mockRedis.del).toHaveBeenCalled();
    expect(result.valkeyKeys).toBe(2);
  });

  it('fullCollect() with no keys returns 0 valkeyKeys', async () => {
    mockRedis.scan = makeScanMock([]);
    const result = await service.fullCollect();
    expect(result.valkeyKeys).toBe(0);
    expect(mockRedis.del).not.toHaveBeenCalled();
  });

  it('fullCollect() returns duration', async () => {
    const result = await service.fullCollect();
    expect(result.duration).toBeGreaterThanOrEqual(0);
  });

  it('sweepOrphans() extracts unique session IDs and collects them', async () => {
    // First scan call returns the global session list; subsequent calls return
    // per-session keys during collect().
    mockRedis.scan = vi
      .fn()
      .mockResolvedValueOnce([
        '0',
        ['mosaic:session:abc:system', 'mosaic:session:abc:messages', 'mosaic:session:xyz:system'],
      ])
      // collect('abc') scan
      .mockResolvedValueOnce(['0', ['mosaic:session:abc:system', 'mosaic:session:abc:messages']])
      // collect('xyz') scan
      .mockResolvedValueOnce(['0', ['mosaic:session:xyz:system']]);
    mockRedis.del.mockResolvedValue(1);

    const result = await service.sweepOrphans();
    expect(result.orphanedSessions).toBeGreaterThanOrEqual(0);
    expect(result.duration).toBeGreaterThanOrEqual(0);
  });

  it('sweepOrphans() returns empty when no session keys', async () => {
    mockRedis.scan = makeScanMock([]);
    const result = await service.sweepOrphans();
    expect(result.orphanedSessions).toBe(0);
    expect(result.totalCleaned).toHaveLength(0);
  });
});
