/**
 * Integration tests for the per-stack Dossier endpoints: blank-on-first-read,
 * upsert persistence, full-document save semantics, field validation, RBAC, and
 * per (node, stack) scoping at the DAO layer.
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import bcrypt from 'bcrypt';
import fs from 'fs';
import path from 'path';
import { setupTestDb, cleanupTestDb, loginAsTestAdmin } from './helpers/setupTestDb';

let tmpDir: string;
let app: import('express').Express;
let DatabaseService: typeof import('../services/DatabaseService').DatabaseService;
let adminCookie: string;
let viewerCookie: string;

const FIELD_KEYS = [
  'purpose', 'owner', 'access_urls', 'static_ip', 'vlan',
  'firewall_notes', 'reverse_proxy_notes', 'backup_notes',
  'upgrade_notes', 'recovery_notes', 'custom_notes',
] as const;

beforeAll(async () => {
  tmpDir = await setupTestDb();
  // The dossier routes require the stack to exist on disk, so seed a compose
  // file for every stack name these tests drive through the HTTP API.
  const composeDir = process.env.COMPOSE_DIR as string;
  for (const stack of ['web', 'upd', 'clr']) {
    fs.mkdirSync(path.join(composeDir, stack), { recursive: true });
    fs.writeFileSync(path.join(composeDir, stack, 'compose.yaml'), 'services:\n  app:\n    image: nginx\n');
  }
  ({ DatabaseService } = await import('../services/DatabaseService'));
  ({ app } = await import('../index'));
  adminCookie = await loginAsTestAdmin(app);

  const viewerHash = await bcrypt.hash('viewerpass', 1);
  DatabaseService.getInstance().addUser({ username: 'viewer', password_hash: viewerHash, role: 'viewer' });
  const viewerRes = await request(app).post('/api/auth/login').send({ username: 'viewer', password: 'viewerpass' });
  const cookies = viewerRes.headers['set-cookie'] as string | string[];
  viewerCookie = Array.isArray(cookies) ? cookies[0] : cookies;
});

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

describe('GET /api/stacks/:stackName/dossier', () => {
  it('rejects unauthenticated requests with 401', async () => {
    const res = await request(app).get('/api/stacks/web/dossier');
    expect(res.status).toBe(401);
  });

  it('returns a blank dossier (200, all fields empty) when none has been saved', async () => {
    const res = await request(app).get('/api/stacks/web/dossier').set('Cookie', adminCookie);
    expect(res.status).toBe(200);
    expect(res.body.stack_name).toBe('web');
    for (const k of FIELD_KEYS) expect(res.body[k]).toBe('');
  });
});

describe('PUT /api/stacks/:stackName/dossier', () => {
  it('persists operator fields and returns the saved row', async () => {
    const res = await request(app)
      .put('/api/stacks/web/dossier')
      .set('Cookie', adminCookie)
      .send({ purpose: 'Reverse proxy', owner: 'ops', static_ip: '10.0.20.5', vlan: '20', firewall_notes: '443 open' });
    expect(res.status).toBe(200);
    expect(res.body.purpose).toBe('Reverse proxy');
    expect(res.body.static_ip).toBe('10.0.20.5');

    const get = await request(app).get('/api/stacks/web/dossier').set('Cookie', adminCookie);
    expect(get.body.owner).toBe('ops');
    expect(get.body.firewall_notes).toBe('443 open');
  });

  it('updates in place: a second PUT keeps a single row and preserves created_at', async () => {
    const first = await request(app).put('/api/stacks/upd/dossier').set('Cookie', adminCookie).send({ purpose: 'one' });
    const createdAt = first.body.created_at as number;
    await new Promise(r => setTimeout(r, 20));
    const second = await request(app).put('/api/stacks/upd/dossier').set('Cookie', adminCookie).send({ purpose: 'two' });
    expect(second.body.purpose).toBe('two');
    expect(second.body.created_at).toBe(createdAt);
    expect(second.body.updated_at).toBeGreaterThanOrEqual(createdAt);

    const row = DatabaseService.getInstance().getDb()
      .prepare("SELECT COUNT(*) AS n FROM stack_dossiers WHERE stack_name = 'upd'").get() as { n: number };
    expect(row.n).toBe(1);
  });

  it('treats a save as a full document: omitted short and block fields are cleared', async () => {
    await request(app).put('/api/stacks/clr/dossier').set('Cookie', adminCookie)
      .send({ purpose: 'p', owner: 'o', firewall_notes: '443 open' });
    const res = await request(app).put('/api/stacks/clr/dossier').set('Cookie', adminCookie)
      .send({ purpose: 'only purpose' });
    expect(res.body.purpose).toBe('only purpose');
    expect(res.body.owner).toBe('');
    expect(res.body.firewall_notes).toBe('');
  });

  it('rejects an over-long field with 400', async () => {
    const res = await request(app)
      .put('/api/stacks/web/dossier')
      .set('Cookie', adminCookie)
      .send({ static_ip: 'x'.repeat(300) });
    expect(res.status).toBe(400);
  });
});

describe('Dossier requires the stack to exist', () => {
  it('returns 404 for a GET on a stack that does not exist', async () => {
    const res = await request(app).get('/api/stacks/ghost/dossier').set('Cookie', adminCookie);
    expect(res.status).toBe(404);
  });

  it('returns 404 for a PUT on a missing stack and creates no orphan row', async () => {
    const res = await request(app).put('/api/stacks/ghost/dossier').set('Cookie', adminCookie).send({ purpose: 'orphan' });
    expect(res.status).toBe(404);
    expect(DatabaseService.getInstance().getStackDossier(1, 'ghost')).toBeUndefined();
  });
});

describe('Dossier RBAC', () => {
  it('lets a viewer read the dossier', async () => {
    const res = await request(app).get('/api/stacks/web/dossier').set('Cookie', viewerCookie);
    expect(res.status).toBe(200);
  });

  it('forbids a viewer from saving the dossier (403)', async () => {
    const res = await request(app).put('/api/stacks/web/dossier').set('Cookie', viewerCookie).send({ purpose: 'nope' });
    expect(res.status).toBe(403);
  });
});

describe('Dossier scoping (DAO)', () => {
  it('keeps dossiers isolated per (node, stack)', () => {
    const db = DatabaseService.getInstance();
    const fields = (purpose: string) => ({
      purpose, owner: '', access_urls: '', static_ip: '', vlan: '',
      firewall_notes: '', reverse_proxy_notes: '', backup_notes: '',
      upgrade_notes: '', recovery_notes: '', custom_notes: '',
    });
    db.upsertStackDossier(1, 'shared', fields('node-1'));
    db.upsertStackDossier(2, 'shared', fields('node-2'));
    expect(db.getStackDossier(1, 'shared')?.purpose).toBe('node-1');
    expect(db.getStackDossier(2, 'shared')?.purpose).toBe('node-2');

    db.deleteStackDossier(1, 'shared');
    expect(db.getStackDossier(1, 'shared')).toBeUndefined();
    expect(db.getStackDossier(2, 'shared')?.purpose).toBe('node-2');
  });
});
