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';
import { CommandAuthorizationService } from '../commands/command-authorization.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: {
      promoteSessionToWarm: ReturnType<typeof vi.fn>;
      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: {
        promoteSessionToWarm: vi.fn().mockResolvedValue(0),
        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('escapes glob metacharacters in a session identifier', async () => {
    await service.collect('abc*?[tenant]\\escape');

    expect(mockRedis.scan).toHaveBeenCalledWith(
      '0',
      'MATCH',
      'mosaic:session:abc\\*\\?\\[tenant\\]\\\\escape:*',
      'COUNT',
      100,
    );
  });

  it('preserves a valid durable approval after session GC', async () => {
    const entries = new Map<string, string>();
    const redis = {
      scan: vi.fn().mockResolvedValue(['0', ['mosaic:session:owned:state']]),
      get: vi.fn(async (key: string) => entries.get(key) ?? null),
      set: vi.fn(async (key: string, value: string) => entries.set(key, value)),
      del: vi.fn(async (...keys: string[]) => {
        let deleted = 0;
        for (const key of keys) deleted += Number(entries.delete(key));
        return deleted;
      }),
    };
    const authorization = new CommandAuthorizationService(
      {
        select: () => ({
          from: () => ({ where: () => ({ limit: async () => [{ role: 'admin' }] }) }),
        }),
      } as never,
      redis,
    );
    const command = {
      name: 'gc',
      description: 'System-wide garbage collection',
      aliases: [],
      scope: 'admin',
      execution: 'socket',
      available: true,
    } as never;
    const payload = { command: 'gc', conversationId: 'owned' };
    const approval = await authorization.createApproval(command, payload, 'admin-1');
    const approvalKey = `interaction:command-approval:${approval!.approvalId}`;
    const gc = new SessionGCService(redis as never, mockLogService as unknown as LogService);

    await gc.collect('owned');

    expect(entries.has(approvalKey)).toBe(true);
    await expect(
      authorization.authorize(command, payload, 'admin-1', approval!.approvalId),
    ).resolves.toEqual({ allowed: true });
  });

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

  it('collect() demotes logs only for the requested session', async () => {
    await service.collect('owned-session');

    expect(mockLogService.logs.promoteSessionToWarm).toHaveBeenCalledWith(
      'owned-session',
      expect.any(Date),
    );
    expect(mockLogService.logs.promoteToWarm).not.toHaveBeenCalled();
  });

  it('does not expose automatic global GC entry points', () => {
    expect('fullCollect' in service).toBe(false);
    expect('sweepOrphans' in service).toBe(false);
  });
});
