import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import express, { type Request, type Response, type NextFunction } from 'express';
import request from 'supertest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Repository } from '../db/repository.js';
import { VapidKeyStore } from '../vapid-store.js';
import { PushService } from '../push-service.js';
import { mountNotificationsApi, resetRateLimitsForTest } from './notifications-api.js';

// Mock web-push so the test never makes real network calls.
vi.mock('web-push', () => {
  const sendNotification = vi.fn().mockResolvedValue({ statusCode: 201 });
  return {
    default: {
      sendNotification,
      setVapidDetails: vi.fn(),
      generateVAPIDKeys: () => ({
        publicKey: 'BPubKeyMaterialMaterialMaterialMaterialMaterialMaterialMaterialMaterialMaterialMaterialMaterialMa',
        privateKey: 'privateKeyMaterialMaterialMaterialMaterial',
      }),
    },
  };
});

const SUBJECT = 'https://aao.example/';

function buildApp(opts: {
  repo: Repository;
  pushService: PushService | null;
  vapidStore: VapidKeyStore | null;
  userId: string;
}): express.Application {
  const app = express();
  const requireAuth = (req: Request, _res: Response, next: NextFunction) => {
    (req as unknown as { user: { id: string; role: string } }).user = {
      id: opts.userId,
      role: 'user',
    };
    next();
  };
  mountNotificationsApi(app, {
    repo: opts.repo,
    pushService: opts.pushService,
    vapidStore: opts.vapidStore,
    requireAuth,
  });
  return app;
}

describe('/api/notifications/*', () => {
  let tempDir = '';
  let repo: Repository;
  let store: VapidKeyStore;
  let service: PushService;
  let app: express.Application;
  let userId = '';

  beforeEach(() => {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-notif-api-'));
    repo = new Repository(join(tempDir, 'db.sqlite'));
    store = new VapidKeyStore(join(tempDir, 'vapid.json'), join(tempDir, 'vapid-history'));
    store.loadOrGenerate(SUBJECT);
    service = new PushService(repo, store);
    const user = repo.createUser({ email: 'u@example.com', name: 'u', role: 'user', status: 'active' });
    userId = user.id;
    app = buildApp({ repo, pushService: service, vapidStore: store, userId });
    resetRateLimitsForTest();
  });

  afterEach(() => {
    repo.close();
    rmSync(tempDir, { recursive: true, force: true });
  });

  describe('GET /vapid-public-key', () => {
    it('returns public key + keyId, never private key', async () => {
      const r = await request(app).get('/api/notifications/vapid-public-key');
      expect(r.status).toBe(200);
      expect(r.body.publicKey).toBeTruthy();
      expect(r.body.keyId).toBeTruthy();
      expect(r.body).not.toHaveProperty('privateKey');
    });

    it('503 when push disabled (no service)', async () => {
      const appOff = buildApp({ repo, pushService: null, vapidStore: null, userId });
      const r = await request(appOff).get('/api/notifications/vapid-public-key');
      expect(r.status).toBe(503);
    });
  });

  describe('subscriptions lifecycle', () => {
    const validBody = {
      endpoint: 'https://fcm.googleapis.com/fcm/send/abc',
      p256dh: 'pubkey',
      auth: 'authsec',
      userAgent: 'Chrome on Pixel',
    };

    it('POST → GET → DELETE full cycle', async () => {
      const post = await request(app).post('/api/notifications/subscriptions').send(validBody);
      expect(post.status).toBe(200);
      const id = post.body.id;

      const list = await request(app).get('/api/notifications/subscriptions');
      expect(list.status).toBe(200);
      expect(list.body.subscriptions).toHaveLength(1);
      expect(list.body.subscriptions[0]).not.toHaveProperty('p256dh');
      expect(list.body.subscriptions[0]).not.toHaveProperty('auth');
      expect(list.body.subscriptions[0]).not.toHaveProperty('endpoint');
      expect(list.body.subscriptions[0].endpointHost).toBe('fcm.googleapis.com');

      const del = await request(app).delete(`/api/notifications/subscriptions/${id}`);
      expect(del.status).toBe(200);

      const list2 = await request(app).get('/api/notifications/subscriptions');
      expect(list2.body.subscriptions).toHaveLength(0);
    });

    it('rejects non-https endpoint', async () => {
      const r = await request(app).post('/api/notifications/subscriptions').send({
        ...validBody, endpoint: 'http://insecure.example/x',
      });
      expect(r.status).toBe(400);
    });

    it('rejects missing p256dh / auth', async () => {
      const r1 = await request(app).post('/api/notifications/subscriptions').send({
        endpoint: validBody.endpoint, auth: 'a',
      });
      expect(r1.status).toBe(400);
      const r2 = await request(app).post('/api/notifications/subscriptions').send({
        endpoint: validBody.endpoint, p256dh: 'p',
      });
      expect(r2.status).toBe(400);
    });

    it('DELETE: cannot delete another user\'s subscription (returns 404)', async () => {
      // create another user with a subscription
      const other = repo.createUser({ email: 'b@example.com', name: 'b', role: 'user', status: 'active' });
      const otherSub = repo.upsertPushSubscription({
        userId: other.id,
        endpoint: 'https://fcm.googleapis.com/fcm/send/other',
        p256dh: 'p', auth: 'a', vapidKeyId: store.getCurrent().keyId,
      });
      const del = await request(app).delete(`/api/notifications/subscriptions/${otherSub.id}`);
      expect(del.status).toBe(404);
      // still there in DB
      expect(repo.getPushSubscriptionById(otherSub.id)).not.toBeNull();
    });

    it('endpoint UNIQUE: re-POST from a different user transfers ownership', async () => {
      // Seed userA's existing subscription with the same endpoint
      const other = repo.createUser({ email: 'a@example.com', name: 'a', role: 'user', status: 'active' });
      repo.upsertPushSubscription({
        userId: other.id, endpoint: validBody.endpoint,
        p256dh: 'oldp', auth: 'olda', vapidKeyId: store.getCurrent().keyId,
      });
      // Current user (userId) re-subscribes the same endpoint
      const r = await request(app).post('/api/notifications/subscriptions').send(validBody);
      expect(r.status).toBe(200);
      const meSubs = repo.listPushSubscriptionsForUser(userId);
      const otherSubs = repo.listPushSubscriptionsForUser(other.id);
      expect(meSubs).toHaveLength(1);
      expect(otherSubs).toHaveLength(0);
    });

    it('rate limit: 11th subscribe in same hour returns 429', async () => {
      for (let i = 0; i < 10; i++) {
        const r = await request(app).post('/api/notifications/subscriptions').send({
          ...validBody, endpoint: `https://fcm.googleapis.com/fcm/send/${i}`,
        });
        expect(r.status).toBe(200);
      }
      const r11 = await request(app).post('/api/notifications/subscriptions').send({
        ...validBody, endpoint: 'https://fcm.googleapis.com/fcm/send/11',
      });
      expect(r11.status).toBe(429);
      expect(r11.body.retryAfter).toBeGreaterThan(0);
    });
  });

  describe('preferences', () => {
    it('GET returns defaults for new users', async () => {
      const r = await request(app).get('/api/notifications/preferences');
      expect(r.status).toBe(200);
      expect(r.body.enabled).toBe(true);
      expect(r.body.events).toEqual({
        running: true, succeeded: true, failed: true, waiting_human: true,
      });
      expect(r.body.includeDetails).toBe(false);
      expect(r.body.v1Migrated).toBe(false);
    });

    it('PUT applies partial update', async () => {
      const r = await request(app).put('/api/notifications/preferences').send({
        enabled: false, events: { succeeded: false },
      });
      expect(r.status).toBe(200);
      expect(r.body.enabled).toBe(false);
      expect(r.body.events.succeeded).toBe(false);
      expect(r.body.events.running).toBe(true);
    });

    it('PUT rejects non-boolean fields', async () => {
      const r = await request(app).put('/api/notifications/preferences').send({
        enabled: 'yes',
      });
      expect(r.status).toBe(400);
    });

    it('migrate-from-localstorage: first call applies, second returns 409', async () => {
      const r1 = await request(app)
        .post('/api/notifications/preferences/migrate-from-localstorage')
        .send({ enabled: true, events: { running: false } });
      expect(r1.status).toBe(200);
      expect(r1.body.prefs.v1Migrated).toBe(true);
      expect(r1.body.prefs.events.running).toBe(false);

      const r2 = await request(app)
        .post('/api/notifications/preferences/migrate-from-localstorage')
        .send({ enabled: false });
      expect(r2.status).toBe(409);
    });
  });

  describe('test endpoint', () => {
    it('returns 200 ok when push enabled', async () => {
      const r = await request(app).post('/api/notifications/test');
      expect(r.status).toBe(200);
      expect(r.body.ok).toBe(true);
    });

    it('503 when push disabled', async () => {
      const appOff = buildApp({ repo, pushService: null, vapidStore: null, userId });
      const r = await request(appOff).post('/api/notifications/test');
      expect(r.status).toBe(503);
    });

    it('rate limit: 6th test in same hour returns 429', async () => {
      for (let i = 0; i < 5; i++) {
        const r = await request(app).post('/api/notifications/test');
        expect(r.status).toBe(200);
      }
      const r6 = await request(app).post('/api/notifications/test');
      expect(r6.status).toBe(429);
    });
  });
});
