/**
 * Integration tests for Alert CRUD endpoints, notification test dispatch
 * validation, and auth enforcement on all alert/notification routes.
 */
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import request from 'supertest';
import bcrypt from 'bcrypt';
import { setupTestDb, cleanupTestDb, loginAsTestAdmin } from './helpers/setupTestDb';

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

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

  // Mock LicenseService so Admiral-gated routes are accessible
  const { LicenseService } = await import('../services/LicenseService');
  vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid');
  vi.spyOn(LicenseService.getInstance(), 'getVariant').mockReturnValue('admiral');
  vi.spyOn(LicenseService.getInstance(), 'getSeatLimits').mockReturnValue({ maxAdmins: null, maxViewers: null });

  ({ app } = await import('../index'));
  authCookie = await loginAsTestAdmin(app);

  // Create a viewer user for non-admin tests
  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);
});

// --- GET /api/alerts ---

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

  it('returns empty array when no alerts exist', async () => {
    const res = await request(app)
      .get('/api/alerts')
      .set('Cookie', authCookie);
    expect(res.status).toBe(200);
    expect(res.body).toEqual([]);
  });

  it('filters alerts by stackName query param', async () => {
    // Seed two alerts for different stacks
    const db = DatabaseService.getInstance();
    db.addStackAlert({ stack_name: 'web', metric: 'cpu_percent', operator: '>', threshold: 80, duration_mins: 5, cooldown_mins: 60 });
    db.addStackAlert({ stack_name: 'api', metric: 'memory_percent', operator: '>', threshold: 90, duration_mins: 5, cooldown_mins: 60 });

    const res = await request(app)
      .get('/api/alerts?stackName=web')
      .set('Cookie', authCookie);
    expect(res.status).toBe(200);
    expect(res.body.length).toBe(1);
    expect(res.body[0].stack_name).toBe('web');
  });
});

// --- POST /api/alerts ---

describe('POST /api/alerts', () => {
  it('rejects unauthenticated requests with 401', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .send({ stack_name: 'test', metric: 'cpu_percent', operator: '>', threshold: 80, duration_mins: 5, cooldown_mins: 60 });
    expect(res.status).toBe(401);
  });

  it('rejects non-admin users with 403', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', viewerCookie)
      .send({ stack_name: 'test', metric: 'cpu_percent', operator: '>', threshold: 80, duration_mins: 5, cooldown_mins: 60 });
    expect(res.status).toBe(403);
  });

  it('creates alert and returns 201 with created resource', async () => {
    const payload = {
      stack_name: 'new-stack',
      metric: 'memory_percent',
      operator: '>=',
      threshold: 85,
      duration_mins: 10,
      cooldown_mins: 30,
    };

    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', authCookie)
      .send(payload);

    expect(res.status).toBe(201);
    expect(res.body.id).toBeDefined();
    expect(res.body.stack_name).toBe('new-stack');
    expect(res.body.metric).toBe('memory_percent');
    expect(res.body.threshold).toBe(85);
  });

  it('validates required fields and returns 400 for missing data', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', authCookie)
      .send({ stack_name: 'test' }); // missing metric, operator, threshold, etc.

    expect(res.status).toBe(400);
    expect(res.body.error).toBe('Invalid alert data');
  });

  it('rejects invalid metric values', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', authCookie)
      .send({ stack_name: 'test', metric: 'invalid_metric', operator: '>', threshold: 80, duration_mins: 5, cooldown_mins: 60 });

    expect(res.status).toBe(400);
  });

  it('rejects negative threshold', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', authCookie)
      .send({ stack_name: 'test', metric: 'cpu_percent', operator: '>', threshold: -1, duration_mins: 5, cooldown_mins: 60 });

    expect(res.status).toBe(400);
  });

  it('rejects empty stack_name', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', authCookie)
      .send({ stack_name: '', metric: 'cpu_percent', operator: '>', threshold: 80, duration_mins: 5, cooldown_mins: 60 });

    expect(res.status).toBe(400);
  });

  it('rejects stack_name exceeding 255 characters', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', authCookie)
      .send({ stack_name: 'a'.repeat(256), metric: 'cpu_percent', operator: '>', threshold: 80, duration_mins: 5, cooldown_mins: 60 });

    expect(res.status).toBe(400);
  });

  it('rejects duration_mins exceeding 1440', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', authCookie)
      .send({ stack_name: 'test', metric: 'cpu_percent', operator: '>', threshold: 80, duration_mins: 1441, cooldown_mins: 60 });

    expect(res.status).toBe(400);
  });

  it('rejects cooldown_mins exceeding 10080', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', authCookie)
      .send({ stack_name: 'test', metric: 'cpu_percent', operator: '>', threshold: 80, duration_mins: 5, cooldown_mins: 10081 });

    expect(res.status).toBe(400);
  });

  it('rejects invalid operator', async () => {
    const res = await request(app)
      .post('/api/alerts')
      .set('Cookie', authCookie)
      .send({ stack_name: 'test', metric: 'cpu_percent', operator: '!=', threshold: 80, duration_mins: 5, cooldown_mins: 60 });

    expect(res.status).toBe(400);
  });
});

// --- DELETE /api/alerts/:id ---

describe('DELETE /api/alerts/:id', () => {
  it('rejects unauthenticated requests with 401', async () => {
    const res = await request(app).delete('/api/alerts/1');
    expect(res.status).toBe(401);
  });

  it('rejects non-admin users with 403', async () => {
    const res = await request(app)
      .delete('/api/alerts/1')
      .set('Cookie', viewerCookie);
    expect(res.status).toBe(403);
  });

  it('deletes existing alert rule', async () => {
    // Create an alert to delete
    const created = DatabaseService.getInstance().addStackAlert({
      stack_name: 'delete-me',
      metric: 'cpu_percent',
      operator: '>',
      threshold: 90,
      duration_mins: 0,
      cooldown_mins: 0,
    });

    const res = await request(app)
      .delete(`/api/alerts/${created.id}`)
      .set('Cookie', authCookie);

    expect(res.status).toBe(200);
    expect(res.body.success).toBe(true);
  });
});

// --- POST /api/notifications/test ---

describe('POST /api/notifications/test', () => {
  it('rejects unauthenticated requests with 401', async () => {
    const res = await request(app)
      .post('/api/notifications/test')
      .send({ type: 'discord', url: 'https://discord.com/api/webhooks/123/abc' });
    expect(res.status).toBe(401);
  });

  it('rejects non-admin users with 403', async () => {
    const res = await request(app)
      .post('/api/notifications/test')
      .set('Cookie', viewerCookie)
      .send({ type: 'discord', url: 'https://discord.com/api/webhooks/123/abc' });
    expect(res.status).toBe(403);
  });

  it('rejects invalid type with 400', async () => {
    const res = await request(app)
      .post('/api/notifications/test')
      .set('Cookie', authCookie)
      .send({ type: 'telegram', url: 'https://example.com' });
    expect(res.status).toBe(400);
    expect(res.body.error).toContain('discord, slack, webhook');
  });

  it('rejects missing type with 400', async () => {
    const res = await request(app)
      .post('/api/notifications/test')
      .set('Cookie', authCookie)
      .send({ url: 'https://example.com' });
    expect(res.status).toBe(400);
  });

  it('rejects non-HTTPS url with 400', async () => {
    const res = await request(app)
      .post('/api/notifications/test')
      .set('Cookie', authCookie)
      .send({ type: 'discord', url: 'http://example.com' });
    expect(res.status).toBe(400);
    expect(res.body.error).toContain('HTTPS');
  });

  it('rejects missing url with 400', async () => {
    const res = await request(app)
      .post('/api/notifications/test')
      .set('Cookie', authCookie)
      .send({ type: 'discord' });
    expect(res.status).toBe(400);
  });

  it('rejects malformed url with 400', async () => {
    const res = await request(app)
      .post('/api/notifications/test')
      .set('Cookie', authCookie)
      .send({ type: 'discord', url: 'https://' });
    expect(res.status).toBe(400);
  });
});
