/**
 * Resolution route integration test — uses a real ESPN NBA box score
 * fetched from
 *   https://site.api.espn.com/apis/site/v2/sports/basketball/nba/summary?event=401859964
 *
 * Workflow:
 *   1. Mock supabase to return three fake unresolved props that match
 *      players in the real fixture.
 *   2. Drive the resolution route with the real box-score JSON.
 *   3. Assert the actual stats were extracted correctly and the
 *      hit/miss/push verdict matches what the line + direction imply.
 */

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

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

// Capture writes the route makes for assertion.
const mockState = {
  unresolved: [],
  inserts: [],
  updates: [],
};

jest.mock('../../src/utils/supabase', () => ({
  getSupabaseServiceClient: () => ({
    from(table) {
      const ctx = { table };
      const proxy = {
        select() { return proxy; },
        eq() { return proxy; },
        in() { return Promise.resolve({ error: null }); },
        is() { return Promise.resolve({ data: mockState.unresolved, error: null }); },
        insert(rows) {
          mockState.inserts.push({ table, rows });
          return Promise.resolve({ error: null });
        },
        update(patch) {
          mockState.updates.push({ table, patch });
          // chain to .eq()/.in() — both close out the call with a resolved promise.
          return {
            eq() { return Promise.resolve({ error: null }); },
            in() { return Promise.resolve({ error: null }); },
          };
        },
      };
      return proxy;
    },
  }),
}));

// Distribution services — stubbed configured=false so side effects are no-ops.
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',
));

// Find a couple of real athletes in the fixture so we can build deterministic
// fake unresolved props. The first team's first athlete is OG Anunoby in the
// saved sample; selecting two players from the fixture removes any need to
// hand-author expected stats — we read them straight from the box.
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,
        starter: !!a.starter,
      });
      if (out.length === n) return out;
    }
  }
  return out;
}

const [p1, p2, p3] = pickAthletes(fixture, 3);
// labels: ['MIN','PTS','FG','3PT','FT','REB','AST','TO','STL','BLK',...]
const p1Points = Number(p1.stats[1]);
const p2Rebounds = Number(p2.stats[5]);
const p3PRA = Number(p3.stats[1]) + Number(p3.stats[5]) + Number(p3.stats[6]);

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

function request(app, method, url, 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 = body ? JSON.stringify(body) : '';
      const req = http.request({
        host: '127.0.0.1',
        port,
        path: url,
        method,
        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); });
      if (data) req.write(data);
      req.end();
    });
  });
}

beforeEach(() => {
  mockState.unresolved = [];
  mockState.inserts = [];
  mockState.updates = [];
});

describe('POST /api/grading/resolve — auth', () => {
  test('rejects without key', async () => {
    const app = makeApp();
    const res = await request(app, 'POST', '/api/grading/resolve', { gameId: 'g', sport: 'nba' });
    expect(res.status).toBe(401);
  });

  test('rejects from non-loopback (handled in real-world by Express trust proxy)', async () => {
    // We can't easily spoof a non-loopback IP locally; auth covers the
    // primary defense. Leaving this stub so the dependency on the IP check
    // is visible if anyone ever rips it out.
    expect(typeof gradingRoutes.__helpers.requireInternal).toBe('function');
  });
});

describe('POST /api/grading/resolve — real ESPN box', () => {
  test('resolves three props against the real box score, returning correct verdicts', async () => {
    mockState.unresolved = [
      { id: 'gh-1', player_id: p1.id, player_name: p1.name, stat_type: 'points', line: p1Points - 0.5, direction: 'over', grade: 'A-', sport: 'nba', projection: p1Points },
      { id: 'gh-2', player_id: p2.id, player_name: p2.name, stat_type: 'rebounds', line: p2Rebounds + 0.5, direction: 'under', grade: 'B+', sport: 'nba', projection: p2Rebounds },
      { id: 'gh-3', player_id: p3.id, player_name: p3.name, stat_type: 'pts_reb_ast', line: p3PRA + 0.5, direction: 'over', grade: 'C', sport: 'nba', projection: p3PRA },
    ];

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

    expect(res.status).toBe(200);
    expect(res.body.resolved + res.body.voided).toBe(3);

    // p1: line is points - 0.5, direction over → actual > line → HIT
    const r1 = res.body.results.find((r) => r.id === 'gh-1');
    expect(r1.actual_value).toBe(p1Points);
    expect(r1.result).toBe('hit');

    // p2: line is rebounds + 0.5, direction under → actual < line → HIT
    const r2 = res.body.results.find((r) => r.id === 'gh-2');
    expect(r2.actual_value).toBe(p2Rebounds);
    expect(r2.result).toBe('hit');

    // p3: combo over, line is sum + 0.5 → actual < line → MISS
    const r3 = res.body.results.find((r) => r.id === 'gh-3');
    expect(r3.actual_value).toBe(p3PRA);
    expect(r3.result).toBe('miss');
  });

  test('marks DNP as void with correction_note', async () => {
    mockState.unresolved = [
      { id: 'dnp', player_id: '999999999', player_name: 'Phantom Player', stat_type: 'points', line: 10.5, direction: 'over', grade: 'C', sport: 'nba' },
    ];
    const app = makeApp();
    const res = await request(app, 'POST', '/api/grading/resolve',
      { gameId: '401859964', sport: 'nba', boxScore: fixture },
      { 'X-VYNDR-Internal-Key': 'int-test-key' },
    );
    expect(res.status).toBe(200);
    expect(res.body.voided).toBe(1);
    expect(res.body.results[0].result).toBe('void');
  });

  test('void: true marks all unresolved props', async () => {
    mockState.unresolved = [
      { id: 'vp-1', player_id: '1', player_name: 'A', stat_type: 'points', line: 10, direction: 'over', grade: 'B', sport: 'nba' },
      { id: 'vp-2', player_id: '2', player_name: 'B', stat_type: 'points', line: 12, direction: 'under', grade: 'C', sport: 'nba' },
    ];
    const app = makeApp();
    const res = await request(app, 'POST', '/api/grading/resolve',
      { gameId: 'pp-game', sport: 'nba', void: true, reason: 'postponed' },
      { 'X-VYNDR-Internal-Key': 'int-test-key' },
    );
    expect(res.status).toBe(200);
    expect(res.body.voided).toBe(2);
  });
});

describe('calculateStat unit cases', () => {
  const { calculateStat } = gradingRoutes.__helpers ?? {};
  const { getSportConfig } = require('../../src/config/sports');

  test('NBA points (index)', () => {
    expect(calculateStat([null, '25', null, null, null], 'points', getSportConfig('nba'))).toBe(25);
  });

  test('NBA threes_made parses makes-attempts string', () => {
    expect(calculateStat([null, null, null, '3-7'], 'threes_made', getSportConfig('nba'))).toBe(3);
    expect(calculateStat([null, null, null, ''], 'threes_made', getSportConfig('nba'))).toBe(0);
    expect(calculateStat([null, null, null, null], 'threes_made', getSportConfig('nba'))).toBe(0);
  });

  test('NBA pts_reb_ast combo', () => {
    const stats = [];
    stats[1] = 22; stats[5] = 9; stats[6] = 5;
    expect(calculateStat(stats, 'pts_reb_ast', getSportConfig('nba'))).toBe(36);
  });

  test('MLB totalBases (mlbField)', () => {
    expect(calculateStat({ totalBases: 4 }, 'totalBases', getSportConfig('mlb'))).toBe(4);
  });

  test('MLB inningsPitched parses "5.1" → 5.33', () => {
    expect(calculateStat({ inningsPitched: '5.1' }, 'inningsPitched', getSportConfig('mlb'))).toBeCloseTo(5.333, 2);
  });

  test('NFL passing_yards (category + field)', () => {
    expect(calculateStat({ passing: { passingYards: 287 } }, 'passing_yards', getSportConfig('nfl'))).toBe(287);
  });

  test('defensive — undefined/null/empty all return 0', () => {
    expect(calculateStat(undefined, 'points', getSportConfig('nba'))).toBe(0);
    expect(calculateStat([], 'points', getSportConfig('nba'))).toBe(0);
    expect(calculateStat({ inningsPitched: null }, 'inningsPitched', getSportConfig('mlb'))).toBe(0);
  });
});
