/**
 * End-to-end learning-loop test.
 *
 *   1. Resolve a prop (hit) through the existing /api/grading/resolve path.
 *   2. Assert CLV computed and stored.
 *   3. Assert accuracy tracker incremented.
 *   4. Assert weight adjuster fired (or correctly skipped because the
 *      sample is thin).
 *   5. Assert the JSONL training logger wrote a line.
 *
 * Stubs all upstream services so the test runs in isolation. The actual
 * Supabase calls land in an in-memory mock that records every operation.
 */

process.env.VYNDR_INTERNAL_KEY = 'lf-test-key';
process.env.NODE_ENV = 'test';

const fs = require('fs');
const path = require('path');

const mockState = {
  unresolved: [],
  inserts: [],
  updates: [],
  closing: null,
  resolutionCount: 100, // enough to pass weight adjuster gate
  accuracyRows: new Map(),
  weightHistory: [],
};

jest.mock('../../src/utils/supabase', () => ({
  getSupabaseServiceClient: () => ({
    from(table) {
      const ctx = { table, filters: {}, countMode: null, head: false };
      const proxy = {
        select(_cols, opts) { ctx.head = !!opts?.head; ctx.countMode = opts?.count; return proxy; },
        eq(col, val) { ctx.filters[col] = val; return proxy; },
        is() { return Promise.resolve({ data: mockState.unresolved, error: null }); },
        in() { return Promise.resolve({ error: null }); },
        gte() { return Promise.resolve({ data: [], error: null }); },
        not() { return proxy; },
        order() { return proxy; },
        limit() { return proxy; },
        single() { return Promise.resolve({ data: { id: 'gid-new' }, error: null }); },
        maybeSingle() {
          if (ctx.table === 'closing_lines') return Promise.resolve({ data: mockState.closing, error: null });
          if (ctx.table === 'grade_history') {
            return Promise.resolve({
              data: { id: ctx.filters.id, sport: 'nba', stat_type: 'points', line: 25.5, direction: 'over', player_id: '1', game_id: 'gm-1' },
              error: null,
            });
          }
          if (ctx.table === 'accuracy_tracking') {
            const k = `${ctx.filters.sport}|${ctx.filters.grade}|${ctx.filters.period}`;
            return Promise.resolve({ data: mockState.accuracyRows.get(k) || null, error: null });
          }
          return Promise.resolve({ data: null, error: null });
        },
        insert(row) { mockState.inserts.push({ table: ctx.table, row }); return Promise.resolve({ error: null }); },
        update(patch) {
          mockState.updates.push({ table: ctx.table, patch, filters: { ...ctx.filters } });
          return {
            eq() { return Promise.resolve({ error: null }); },
            in() { return Promise.resolve({ error: null }); },
          };
        },
        upsert(row) {
          if (ctx.table === 'accuracy_tracking') {
            mockState.accuracyRows.set(`${row.sport}|${row.grade}|${row.period}`, { ...row });
          }
          return Promise.resolve({ error: null });
        },
        then(resolve) {
          if (ctx.table === 'resolution_results' && ctx.countMode === 'exact') {
            return resolve({ count: mockState.resolutionCount, error: null });
          }
          if (ctx.table === 'engine1_weights') {
            const matches = mockState.weightHistory.filter((r) => {
              for (const [k, v] of Object.entries(ctx.filters)) {
                if (r[k] !== v) return false;
              }
              return true;
            }).sort((a, b) => b.version - a.version);
            return resolve({ data: matches, error: null });
          }
          return resolve({ data: [], error: null });
        },
      };
      return proxy;
    },
  }),
}));

jest.mock('../../src/services/distribution/webPush', () => ({
  configured: () => false, sendPushToSport: async () => ({ ok: true, sent: 0 }),
}));
jest.mock('../../src/services/distribution/telegram', () => ({
  configured: () => false, postToTelegram: async () => ({ ok: true }),
}));
jest.mock('../../src/services/distribution/discord', () => ({
  webhookFor: () => null, postToDiscord: async () => ({ ok: true }),
}));

const express = require('express');
const gradingRoutes = require('../../src/routes/grading');

const fixture = JSON.parse(fs.readFileSync(
  path.join(__dirname, '..', 'fixtures', 'nba-boxscore-sample.json'),
  'utf8',
));

function pickAthletes(box, n) {
  const out = [];
  for (const team of box.boxscore.players) {
    for (const a of team.statistics[0].athletes) {
      out.push({ id: String(a.athlete.id), name: a.athlete.displayName, stats: a.stats });
      if (out.length === n) return out;
    }
  }
  return out;
}

function makeApp() {
  const app = express();
  app.use(express.json({ limit: '2mb' }));
  app.use('/api/grading', gradingRoutes);
  return app;
}

function request(app, body, headers = {}) {
  return new Promise((resolve, reject) => {
    const http = require('http');
    const server = app.listen(0, '127.0.0.1', () => {
      const port = server.address().port;
      const data = JSON.stringify(body);
      const req = http.request({
        host: '127.0.0.1', port, path: '/api/grading/resolve', method: 'POST',
        headers: { 'Content-Type': 'application/json', ...headers },
      }, (res) => {
        const chunks = [];
        res.on('data', (c) => chunks.push(c));
        res.on('end', () => {
          server.close();
          const raw = Buffer.concat(chunks).toString('utf8');
          let parsed; try { parsed = JSON.parse(raw); } catch { parsed = raw; }
          resolve({ status: res.statusCode, body: parsed });
        });
      });
      req.on('error', (err) => { server.close(); reject(err); });
      req.write(data);
      req.end();
    });
  });
}

beforeEach(() => {
  mockState.unresolved = [];
  mockState.inserts.length = 0;
  mockState.updates.length = 0;
  mockState.closing = { id: 'cl-1', pinnacle_line: 27.5 };
  mockState.resolutionCount = 100;
  mockState.accuracyRows.clear();
  mockState.weightHistory.length = 0;
});

describe('learning loop — post-resolution side effects', () => {
  test('hit on an A-grade prop drives CLV + accuracy + weights', async () => {
    const [p1] = pickAthletes(fixture, 1);
    const p1Points = Number(p1.stats[1]);
    mockState.unresolved = [{
      id: 'gh-A',
      player_id: p1.id,
      player_name: p1.name,
      stat_type: 'points',
      line: p1Points - 0.5,
      direction: 'over',
      grade: 'A',
      sport: 'nba',
      projection: p1Points,
      factors: ['l5_hot_vs_line', 'home_game'],
    }];

    const app = makeApp();
    const res = await request(app, {
      gameId: '401859964',
      sport: 'nba',
      boxScore: fixture,
    }, { 'X-VYNDR-Internal-Key': 'lf-test-key' });

    expect(res.status).toBe(200);
    expect(res.body.resolved).toBe(1);

    // Side effects are fire-and-forget; give them a tick to settle.
    await new Promise((r) => setImmediate(r));
    await new Promise((r) => setTimeout(r, 30));

    // Accuracy upserted for all three periods.
    const periods = Array.from(mockState.accuracyRows.keys());
    expect(periods).toContain('nba|A|all_time');
    expect(periods).toContain('nba|A|last_30d');
    expect(periods).toContain('nba|A|last_7d');
    const allTime = mockState.accuracyRows.get('nba|A|all_time');
    expect(allTime.total_hit + allTime.total_miss).toBeGreaterThan(0);

    // CLV: an update on grade_history with a clv field was queued by the
    // computeCLV path.
    const clvUpdates = mockState.updates.filter((u) => 'clv' in (u.patch || {}));
    expect(clvUpdates.length).toBeGreaterThan(0);
    expect(clvUpdates[0].patch.clv).toBeGreaterThan(0); // graded line was lower → positive CLV

    // Weight adjuster inserts at least one engine1_weights row (factors > 0).
    const weightInserts = mockState.inserts.filter((i) => i.table === 'engine1_weights');
    expect(weightInserts.length).toBeGreaterThan(0);
  });

  test('void result records accuracy but not CLV / weights', async () => {
    mockState.unresolved = [{
      id: 'gh-void',
      player_id: '99999',
      player_name: 'Phantom',
      stat_type: 'points',
      line: 10.5,
      direction: 'over',
      grade: 'B',
      sport: 'nba',
    }];
    const app = makeApp();
    await request(app, {
      gameId: '401859964',
      sport: 'nba',
      boxScore: fixture,
    }, { 'X-VYNDR-Internal-Key': 'lf-test-key' });
    await new Promise((r) => setTimeout(r, 30));

    expect(mockState.accuracyRows.get('nba|B|all_time')).toBeTruthy();
    // CLV + weight adjustments should not have fired on a void.
    const clvUpdates = mockState.updates.filter((u) => 'clv' in (u.patch || {}));
    expect(clvUpdates.length).toBe(0);
    const weightInserts = mockState.inserts.filter((i) => i.table === 'engine1_weights');
    expect(weightInserts.length).toBe(0);
  });
});
