/**
 * Integration tests for cached HTTP endpoints:
 *   - /api/stats (2s TTL, invalidated on writes)
 *   - /api/system/stats (3s TTL, no write invalidation)
 *   - /api/stacks/statuses (3s TTL, invalidated on writes)
 *   - /api/system/cache-stats (admin observability)
 *
 * Verifies cache hit behavior (second call does not re-invoke the
 * underlying Docker / si / FileSystem work), write-path invalidation
 * (POST /api/stacks resets the cache), and that the admin endpoint
 * reports per-namespace counters.
 *
 * The tests mock DockerController / FileSystemService at the service
 * layer rather than hitting the real Docker socket, so they run in CI
 * without requiring any external daemon.
 */
import { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest';
import request from 'supertest';
import { setupTestDb, cleanupTestDb, TEST_USERNAME, TEST_PASSWORD } from './helpers/setupTestDb';

// ── Hoisted mocks (must come before importing the app) ─────────────────

const {
  mockGetAllContainers,
  mockGetBulkStackStatuses,
  mockGetStacks,
  mockCurrentLoad,
  mockMem,
  mockFsSize,
} = vi.hoisted(() => ({
  mockGetAllContainers: vi.fn(),
  mockGetBulkStackStatuses: vi.fn(),
  mockGetStacks: vi.fn(),
  mockCurrentLoad: vi.fn(),
  mockMem: vi.fn(),
  mockFsSize: vi.fn(),
}));

vi.mock('../services/DockerController', async () => {
  const actual = await vi.importActual<typeof import('../services/DockerController')>('../services/DockerController');
  return {
    ...actual,
    default: {
      ...actual.default,
      getInstance: () => ({
        getAllContainers: mockGetAllContainers,
        getBulkStackStatuses: mockGetBulkStackStatuses,
      }),
    },
    globalDockerNetwork: { rxSec: 0, txSec: 0 },
  };
});

vi.mock('../services/FileSystemService', () => ({
  FileSystemService: {
    getInstance: () => ({
      getStacks: mockGetStacks,
      createStack: vi.fn().mockResolvedValue(undefined),
      getBaseDir: () => '/tmp/compose',
    }),
  },
}));

vi.mock('systeminformation', () => ({
  default: {
    currentLoad: (...args: unknown[]) => mockCurrentLoad(...args),
    mem: (...args: unknown[]) => mockMem(...args),
    fsSize: (...args: unknown[]) => mockFsSize(...args),
  },
  currentLoad: (...args: unknown[]) => mockCurrentLoad(...args),
  mem: (...args: unknown[]) => mockMem(...args),
  fsSize: (...args: unknown[]) => mockFsSize(...args),
}));

// ── Setup ──────────────────────────────────────────────────────────────

let tmpDir: string;
let app: import('express').Express;
let authCookie: string;
let CacheService: typeof import('../services/CacheService').CacheService;

beforeAll(async () => {
  tmpDir = await setupTestDb();
  ({ app } = await import('../index'));
  ({ CacheService } = await import('../services/CacheService'));

  const login = await request(app)
    .post('/api/auth/login')
    .send({ username: TEST_USERNAME, password: TEST_PASSWORD });
  authCookie = (login.headers['set-cookie'] as unknown as string[])[0];
});

afterAll(() => {
  cleanupTestDb(tmpDir);
});

beforeEach(() => {
  CacheService.getInstance().flush();

  mockGetAllContainers.mockReset();
  mockGetBulkStackStatuses.mockReset();
  mockGetStacks.mockReset();
  mockCurrentLoad.mockReset();
  mockMem.mockReset();
  mockFsSize.mockReset();

  mockGetAllContainers.mockResolvedValue([
    { State: 'running', Labels: { 'com.docker.compose.project.working_dir': '/tmp/compose/a' } },
    { State: 'exited', Labels: {} },
  ]);
  mockGetBulkStackStatuses.mockResolvedValue({});
  mockGetStacks.mockResolvedValue([]);
  mockCurrentLoad.mockResolvedValue({ currentLoad: 42.5, cpus: [{}, {}] });
  mockMem.mockResolvedValue({ total: 1000, used: 500, free: 500 });
  mockFsSize.mockResolvedValue([{ fs: '/dev/sda1', mount: '/', size: 1000, used: 500, available: 500, use: 50 }]);
});

// ── /api/stats ─────────────────────────────────────────────────────────

describe('GET /api/stats caching', () => {
  it('returns shape { active, managed, unmanaged, exited, total }', async () => {
    const res = await request(app).get('/api/stats').set('Cookie', authCookie);
    expect(res.status).toBe(200);
    expect(res.body).toHaveProperty('active');
    expect(res.body).toHaveProperty('managed');
    expect(res.body).toHaveProperty('unmanaged');
    expect(res.body).toHaveProperty('exited');
    expect(res.body).toHaveProperty('total');
  });

  it('serves the second call from cache without re-invoking Docker', async () => {
    await request(app).get('/api/stats').set('Cookie', authCookie);
    await request(app).get('/api/stats').set('Cookie', authCookie);
    expect(mockGetAllContainers).toHaveBeenCalledTimes(1);
  });

  it('invalidates on POST /api/stacks', async () => {
    await request(app).get('/api/stats').set('Cookie', authCookie);
    expect(mockGetAllContainers).toHaveBeenCalledTimes(1);

    const create = await request(app)
      .post('/api/stacks')
      .set('Cookie', authCookie)
      .send({ stackName: 'new-stack' });
    expect(create.status).toBe(200);

    await request(app).get('/api/stats').set('Cookie', authCookie);
    expect(mockGetAllContainers).toHaveBeenCalledTimes(2);
  });
});

// ── /api/system/stats ──────────────────────────────────────────────────

describe('GET /api/system/stats caching', () => {
  it('collapses concurrent calls so si.currentLoad() runs once', async () => {
    // Two back-to-back requests, the second should hit the cache.
    await request(app).get('/api/system/stats').set('Cookie', authCookie);
    await request(app).get('/api/system/stats').set('Cookie', authCookie);
    expect(mockCurrentLoad).toHaveBeenCalledTimes(1);
    expect(mockMem).toHaveBeenCalledTimes(1);
    expect(mockFsSize).toHaveBeenCalledTimes(1);
  });

  it('response includes network block that is read per-request outside the cache', async () => {
    const res1 = await request(app).get('/api/system/stats').set('Cookie', authCookie);
    const res2 = await request(app).get('/api/system/stats').set('Cookie', authCookie);
    expect(res1.body).toHaveProperty('network');
    expect(res2.body).toHaveProperty('network');
    // CPU/mem/disk sample is cached; network is fresh per request.
    expect(mockCurrentLoad).toHaveBeenCalledTimes(1);
  });
});

// ── /api/stacks/statuses ───────────────────────────────────────────────

describe('GET /api/stacks/statuses caching', () => {
  it('serves repeat calls from cache without re-invoking the filesystem', async () => {
    mockGetStacks.mockResolvedValue(['web', 'db']);
    mockGetBulkStackStatuses.mockResolvedValue({
      web: { status: 'running' },
      db: { status: 'running' },
    });

    await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
    await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);

    expect(mockGetStacks).toHaveBeenCalledTimes(1);
    expect(mockGetBulkStackStatuses).toHaveBeenCalledTimes(1);
  });

  it('invalidates on POST /api/stacks', async () => {
    mockGetStacks.mockResolvedValue(['web']);
    mockGetBulkStackStatuses.mockResolvedValue({ web: { status: 'running' } });

    await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
    expect(mockGetStacks).toHaveBeenCalledTimes(1);

    await request(app)
      .post('/api/stacks')
      .set('Cookie', authCookie)
      .send({ stackName: 'fresh-stack' });

    await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
    expect(mockGetStacks).toHaveBeenCalledTimes(2);
  });
});

// ── /api/system/cache-stats ────────────────────────────────────────────

describe('GET /api/system/cache-stats', () => {
  it('requires admin auth', async () => {
    const res = await request(app).get('/api/system/cache-stats');
    expect(res.status).toBe(401);
  });

  it('returns per-namespace hit/miss/stale counters', async () => {
    // Generate some cache traffic first.
    await request(app).get('/api/stats').set('Cookie', authCookie); // miss
    await request(app).get('/api/stats').set('Cookie', authCookie); // hit
    await request(app).get('/api/system/stats').set('Cookie', authCookie); // miss

    const res = await request(app).get('/api/system/cache-stats').set('Cookie', authCookie);
    expect(res.status).toBe(200);
    expect(res.body.stats).toBeDefined();
    expect(res.body.stats.hits).toBeGreaterThanOrEqual(1);
    expect(res.body.stats.misses).toBeGreaterThanOrEqual(1);
    expect(res.body['system-stats']).toBeDefined();
  });
});
