/**
 * DS1 (DESIGN-SPEC §17) — scan→ledger persistence.
 *
 * Proves a completed scan's ledger row (built by the SAME builder the
 * /api/scan route uses) is readable by the /api/ledger/mine query: scoped by
 * user_id, keyed by player_key = nameKey, deduped idempotently. This is the
 * "every grade, no hiding" guarantee the audit found broken.
 */

const { buildManualLedgerRow, LEDGER_CONFLICT_COLS, gameDateET } = require('../../web/src/lib/ledgerRow');
const { nameKey, normalizeName } = require('../../web/src/lib/playerName');

// A minimal in-memory stand-in for the service-role Supabase client, modelling
// exactly the two operations that matter: the route's upsert(row, {onConflict,
// ignoreDuplicates}) and the /api/ledger/mine read
// (.select().eq('user_id',id).order('graded_at',desc).limit(n)).
function makeFakeSupabase() {
  const rows = [];
  const conflictCols = LEDGER_CONFLICT_COLS.split(',');
  const keyOf = (r) => conflictCols.map((c) => String(r[c])).join('|');

  return {
    _rows: rows,
    from() {
      return {
        async upsert(row, opts = {}) {
          const k = keyOf(row);
          const existing = rows.find((r) => keyOf(r) === k);
          if (existing) {
            if (!opts.ignoreDuplicates) Object.assign(existing, row);
            return { data: null, error: null };
          }
          rows.push({ ...row, id: `row-${rows.length + 1}` });
          return { data: null, error: null };
        },
        // Chainable read that mirrors the mine route.
        select() {
          const filters = {};
          let orderCol = null; let asc = true; let lim = Infinity;
          const q = {
            eq(col, val) { filters[col] = val; return q; },
            order(col, o = {}) { orderCol = col; asc = o.ascending !== false; return q; },
            limit(n) { lim = n; return q; },
            then(resolve) {
              let out = rows.filter((r) => Object.entries(filters).every(([c, v]) => r[c] === v));
              if (orderCol) {
                out = [...out].sort((a, b) => (a[orderCol] < b[orderCol] ? -1 : a[orderCol] > b[orderCol] ? 1 : 0));
                if (!asc) out.reverse();
              }
              out = out.slice(0, lim);
              resolve({ data: out, error: null });
            },
          };
          return q;
        },
      };
    },
  };
}

// The mine query, byte-for-byte in spirit with routes/ledger.js GET /mine.
async function mineQuery(sb, userId, limit = 100) {
  const { data } = await sb.from('ledger_entries')
    .select('*')
    .eq('user_id', userId)
    .order('graded_at', { ascending: false })
    .limit(limit);
  return data;
}

describe('scan→ledger persistence (DS1 §17)', () => {
  const USER_A = 'user-aaaa';
  const USER_B = 'user-bbbb';

  test('a completed scan write is immediately readable by the mine query', async () => {
    const sb = makeFakeSupabase();
    const row = buildManualLedgerRow({
      userId: USER_A,
      sport: 'MLB',
      player: 'Aaron Judge',
      stat: 'home_runs',
      line: 1.5,
      side: 'over',
      book: 'draftkings',
      grade: 'A',
      confidence: 71,
      projection: 1.9,
      edge: 12.4,
    });

    await sb.from('ledger_entries').upsert(row, { onConflict: LEDGER_CONFLICT_COLS, ignoreDuplicates: true });

    const mine = await mineQuery(sb, USER_A);
    expect(mine).toHaveLength(1);
    const got = mine[0];
    expect(got.user_id).toBe(USER_A);
    // The join key the ledger/model reads on is player_key = nameKey.
    expect(got.player_key).toBe(nameKey('Aaron Judge'));
    expect(got.player_name).toBe(normalizeName('Aaron Judge').display);
    expect(got.sport).toBe('mlb');
    expect(got.stat).toBe('home_runs');
    expect(got.side).toBe('over');
    expect(got.line).toBe(1.5);
    expect(got.grade).toBe('A');
    // game_id embeds the ET game_date so the settle pass can find it.
    expect(got.game_id).toBe(`manual:mlb:${gameDateET()}:${nameKey('Aaron Judge')}`);
    expect(got.game_date).toBe(gameDateET());
  });

  test('the mine query is scoped: another user cannot read the row', async () => {
    const sb = makeFakeSupabase();
    const row = buildManualLedgerRow({
      userId: USER_A, sport: 'MLB', player: 'Shohei Ohtani', stat: 'total_bases',
      line: 1.5, side: 'over', book: 'fanduel', grade: 'B',
    });
    await sb.from('ledger_entries').upsert(row, { onConflict: LEDGER_CONFLICT_COLS, ignoreDuplicates: true });

    expect(await mineQuery(sb, USER_A)).toHaveLength(1);
    expect(await mineQuery(sb, USER_B)).toHaveLength(0);
  });

  test('re-scanning the same prop is idempotent (never duplicates the row)', async () => {
    const sb = makeFakeSupabase();
    const mk = () => buildManualLedgerRow({
      userId: USER_A, sport: 'MLB', player: 'Mookie Betts', stat: 'hits',
      line: 0.5, side: 'over', book: 'draftkings', grade: 'A',
    });
    await sb.from('ledger_entries').upsert(mk(), { onConflict: LEDGER_CONFLICT_COLS, ignoreDuplicates: true });
    await sb.from('ledger_entries').upsert(mk(), { onConflict: LEDGER_CONFLICT_COLS, ignoreDuplicates: true });

    expect(await mineQuery(sb, USER_A)).toHaveLength(1);
  });

  test('DATA SEMANTICS: absent beats wrong — no snapshot ⇒ locked_odds/team null, model fields carried', () => {
    const row = buildManualLedgerRow({
      userId: USER_A, sport: 'WNBA', player: "A'ja Wilson", stat: 'points',
      line: 22.5, side: 'over', grade: 'B', confidence: 60, projection: 24,
      // no team / lockedOdds supplied (no snapshot match)
    });
    expect(row.locked_odds).toBeNull();
    expect(row.team).toBeNull();
    expect(row.opponent).toBeNull();
    expect(row.model_value).toBe(24);
    expect(row.confidence).toBe(60);
    // Non-numeric model fields degrade to null, never 0 (Number(null)===0 bug).
    expect(buildManualLedgerRow({ userId: USER_A, sport: 'MLB', player: 'X', stat: 'hits', line: 1, side: 'over' }).confidence).toBeNull();
    expect(buildManualLedgerRow({ userId: USER_A, sport: 'MLB', player: 'X', stat: 'hits', line: 1, side: 'over' }).model_value).toBeNull();
  });

  test('the dedupe key matches the migration-019 constraint the route upserts on', () => {
    expect(LEDGER_CONFLICT_COLS).toBe('user_id,player_key,stat,line,side,game_id');
  });
});
