'use strict';

// Session S7 (a1) — THE VYNDR REPORT routes. Redis is mocked (app.js pulls it
// in transitively); Listmonk is a mocked global fetch. NOTE: the subscribe
// route sits behind a 10/min IP limiter with no reset hook (same lesson as
// analyze.test.js) — this file stays well under 10 subscribe requests.

const request = require('supertest');

let mockStore = {};
jest.mock('../../src/utils/redis', () => ({
  getRedisClient: () => ({}),
  cacheGet: async (k) => (k in mockStore ? mockStore[k] : null),
  cacheSet: async (k, v) => { mockStore[k] = v; return true; },
  cacheDel: async () => true,
  isDegraded: () => false,
}));

const app = require('../../src/app');

const LISTMONK_ENV = {
  LISTMONK_URL: 'http://127.0.0.1:9000',
  LISTMONK_USER: 'vyndr-api',
  LISTMONK_TOKEN: 'tok123',
  LISTMONK_LIST_ID: '3',
};
const ENV_KEYS = [...Object.keys(LISTMONK_ENV), 'VYNDR_INTERNAL_KEY',
  // Hermetic: a developer .env with real Supabase creds must not make
  // ledgerService.getModelAggregate reach for the network in this suite.
  'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_SERVICE_KEY'];

const savedEnv = {};
beforeAll(() => { for (const k of ENV_KEYS) savedEnv[k] = process.env[k]; });
afterAll(() => {
  for (const k of ENV_KEYS) {
    if (savedEnv[k] === undefined) delete process.env[k];
    else process.env[k] = savedEnv[k];
  }
});

const realFetch = globalThis.fetch;
beforeEach(() => {
  mockStore = {};
  for (const k of ENV_KEYS) delete process.env[k];
});
afterEach(() => { globalThis.fetch = realFetch; });

describe('POST /api/newsletter/subscribe', () => {
  test('no Listmonk env → calm HTTP 200 not-configured (never a scary error)', async () => {
    globalThis.fetch = jest.fn();
    const res = await request(app)
      .post('/api/newsletter/subscribe')
      .send({ email: 'kev@example.com' });
    expect(res.status).toBe(200);
    expect(res.body).toEqual({ ok: false, reason: 'not configured' });
    expect(globalThis.fetch).not.toHaveBeenCalled();
  });

  test('honeypot filled → silent ok, nothing forwarded', async () => {
    Object.assign(process.env, LISTMONK_ENV);
    globalThis.fetch = jest.fn();
    const res = await request(app)
      .post('/api/newsletter/subscribe')
      .send({ email: 'bot@example.com', website: 'http://spam.example' });
    expect(res.status).toBe(200);
    expect(res.body).toEqual({ ok: true });
    expect(globalThis.fetch).not.toHaveBeenCalled();
  });

  test('invalid email → 400 with a plain message', async () => {
    const res = await request(app)
      .post('/api/newsletter/subscribe')
      .send({ email: 'not-an-email' });
    expect(res.status).toBe(400);
    expect(res.body.ok).toBe(false);
    expect(res.body.error).toBe('Enter a valid email.');
  });

  test('env set → forwards to Listmonk with double opt-in (preconfirm false)', async () => {
    Object.assign(process.env, LISTMONK_ENV);
    globalThis.fetch = jest.fn(async () => ({ ok: true, status: 200, json: async () => ({}) }));
    const res = await request(app)
      .post('/api/newsletter/subscribe')
      .send({ email: 'Kev@Example.com' });
    expect(res.status).toBe(200);
    expect(res.body).toEqual({ ok: true });

    expect(globalThis.fetch).toHaveBeenCalledTimes(1);
    const [url, opts] = globalThis.fetch.mock.calls[0];
    expect(url).toBe('http://127.0.0.1:9000/api/subscribers');
    expect(opts.headers.Authorization).toBe('token vyndr-api:tok123');
    const body = JSON.parse(opts.body);
    expect(body.email).toBe('kev@example.com');
    expect(body.lists).toEqual([3]);
    expect(body.preconfirm_subscriptions).toBe(false);
  });

  test('Listmonk 409 (already subscribed) → still ok, no enumeration', async () => {
    Object.assign(process.env, LISTMONK_ENV);
    globalThis.fetch = jest.fn(async () => ({ ok: false, status: 409, json: async () => ({}) }));
    const res = await request(app)
      .post('/api/newsletter/subscribe')
      .send({ email: 'kev@example.com' });
    expect(res.status).toBe(200);
    expect(res.body).toEqual({ ok: true });
  });

  test('Listmonk down → calm HTTP 200 unavailable', async () => {
    Object.assign(process.env, LISTMONK_ENV);
    globalThis.fetch = jest.fn(async () => { throw new Error('ECONNREFUSED'); });
    const res = await request(app)
      .post('/api/newsletter/subscribe')
      .send({ email: 'kev@example.com' });
    expect(res.status).toBe(200);
    expect(res.body).toEqual({ ok: false, reason: 'unavailable' });
  });
});

describe('POST /api/internal/newsletter/send', () => {
  test('requires the internal key', async () => {
    process.env.VYNDR_INTERNAL_KEY = 'shh';
    const res = await request(app).post('/api/internal/newsletter/send').send({});
    expect(res.status).toBe(401);
  });

  test('with key but no Listmonk env → graceful no-op (operator sees why)', async () => {
    process.env.VYNDR_INTERNAL_KEY = 'shh';
    globalThis.fetch = jest.fn();
    const res = await request(app)
      .post('/api/internal/newsletter/send')
      .set('x-internal-key', 'shh')
      .send({});
    expect(res.status).toBe(200);
    expect(res.body).toEqual({ ok: false, reason: 'not configured' });
    expect(globalThis.fetch).not.toHaveBeenCalled();
  });

  test('with key + env but an empty pipeline → refuses to send an empty report', async () => {
    process.env.VYNDR_INTERNAL_KEY = 'shh';
    Object.assign(process.env, LISTMONK_ENV);
    globalThis.fetch = jest.fn();
    // mockStore is empty → no snapshots, no roster logs → empty report.
    const res = await request(app)
      .post('/api/internal/newsletter/send')
      .set('x-internal-key', 'shh')
      .send({ sports: ['mlb'] });
    expect(res.status).toBe(200);
    expect(res.body.ok).toBe(false);
    expect(res.body.reason).toBe('empty report');
    expect(globalThis.fetch).not.toHaveBeenCalled();
  });
});
