const request = require('supertest');

// Mock Redis — covers both the legacy `getRedisClient()` surface and
// the cacheGet/cacheSet helpers added in Session 6c (used by /api/analyze
// cache from Session 7d).
const mockRedis = {
  get: jest.fn(),
  set: jest.fn(),
  hset: jest.fn(),
  hgetall: jest.fn(),
  expire: jest.fn(),
};
jest.mock('../../src/utils/redis', () => ({
  getRedisClient: () => mockRedis,
  cacheGet: async () => null,
  cacheSet: async () => true,
  cacheDel: async () => true,
  isDegraded: () => false,
}));

// ARCH-1 (Session 7g): the route now grades through engine1 via the
// analyzeViaEngine1 helper. We mock the helper directly because it owns
// the upstream chain — there's no value in driving the chain through
// the test now that the only legacy fallback path is gone.
//
// Every mocked return is a fully-shaped legacy response so the route
// (which still does cache wrap + _cache: 'MISS|HIT' tagging) is the
// only thing under test.
const mockAnalyzeViaEngine1 = jest.fn();
jest.mock('../../src/services/intelligence/analyzeViaEngine1', () => ({
  analyzeViaEngine1: (...args) => mockAnalyzeViaEngine1(...args),
}));

// Session 7h: the route now layers free-tier response gating on top of
// the engine output. This suite tests the ENGINE → route contract
// (full shape, kill conditions, etc.); the gating layer has its own
// dedicated tests in tests/unit/tierGating.test.js. Pass-through here.
jest.mock('../../src/utils/tierGating', () => ({
  applyTierGating: (result) => result,
}));

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

function fullShapedResponse(overrides = {}) {
  return {
    player: 'Nikola Jokic',
    stat_type: 'points',
    line: 26.5,
    direction: 'over',
    book: 'draftkings',
    grade: 'A',
    confidence: 78,
    edge_pct: 6.2,
    kill_conditions_triggered: [],
    reasoning: {
      summary: 'Concrete sentence about Jokic averaging 28.4 last 5.',
      steps: {
        season_avg: { value: 26.3, vs_line: -0.2, signal: null },
        recent_form: { value: 28.1, vs_line: 1.6, signal: null },
        situational: {
          home_away: { value: null, context: 'home', signal: null },
          rest_days: { value: 2, context: 'rested', signal: null },
          vs_opponent: { value: null, games: null, signal: null },
        },
        line_comparison: { best_line: null, worst_line: null, edge_from_best: 0, signal: null },
        kill_conditions: [],
        final_grade: 'A',
        narrative: [{ step: 1, detail: 'placeholder' }],
      },
    },
    ...overrides,
  };
}

// Session 7h: scan-limit middleware mounts on /api/analyze with a 24h
// rolling per-IP quota. Without resetting, supertest's loopback IP
// burns its 3 free-tier slots inside this file and later cases 429.
const { __internals: scanLimitInternals } = require('../../src/middleware/scanLimit');

beforeEach(() => {
  jest.clearAllMocks();
  mockRedis.get.mockResolvedValue(null);
  mockRedis.set.mockResolvedValue('OK');
  mockAnalyzeViaEngine1.mockReset();
  scanLimitInternals.resetForTests();
});

describe('POST /api/analyze/prop', () => {
  it('returns complete analysis with all fields', async () => {
    mockAnalyzeViaEngine1.mockResolvedValueOnce(fullShapedResponse({ confidence: 78 }));

    const res = await request(app)
      .post('/api/analyze/prop')
      .send({
        player: 'Nikola Jokic',
        stat_type: 'points',
        line: 26.5,
        direction: 'over',
        book: 'draftkings',
      })
      .expect(200);

    // SHAPE assertions (must survive engine swap — these are the API contract):
    expect(res.body.player).toBe('Nikola Jokic');
    expect(res.body.stat_type).toBe('points');
    expect(res.body.grade).toMatch(/^[ABCDF]$/);
    expect(typeof res.body.edge_pct).toBe('number');
    expect(typeof res.body.confidence).toBe('number');
    expect(res.body.confidence).toBeGreaterThanOrEqual(0);
    expect(res.body.confidence).toBeLessThanOrEqual(100);
    expect(Array.isArray(res.body.kill_conditions_triggered)).toBe(true);
    expect(res.body.reasoning).toBeDefined();
    expect(res.body.reasoning.summary).toBeDefined();
    expect(res.body.reasoning.steps).toBeDefined();
    expect(res.body.reasoning.steps.season_avg).toBeDefined();
    expect(res.body.reasoning.steps.recent_form).toBeDefined();
    expect(res.body.reasoning.steps.situational).toBeDefined();
    expect(res.body.reasoning.steps.line_comparison).toBeDefined();
    expect(res.body.reasoning.steps.kill_conditions).toBeDefined();
    expect(res.body.reasoning.steps.final_grade).toBeDefined();
    // PERF-1 cache tag — every fresh call is MISS.
    expect(res.body._cache).toBe('MISS');
  });

  it('returns grade A/B for player averaging above line with good recent form', async () => {
    mockAnalyzeViaEngine1.mockResolvedValueOnce(fullShapedResponse({
      grade: 'A',
      confidence: 82,
      edge_pct: 8.4,
    }));

    const res = await request(app)
      .post('/api/analyze/prop')
      .send({
        player: 'Nikola Jokic',
        stat_type: 'points',
        line: 24.5,
        direction: 'over',
        book: 'draftkings',
      })
      .expect(200);

    expect(['A', 'B']).toContain(res.body.grade);
    expect(res.body.edge_pct).toBeGreaterThan(0);
  });

  it('returns a low grade for player averaging below line', async () => {
    // Engine 1 collapses A+/A/A- → A and (D|F) → D|F. For a cold prop the
    // adapter produces D or F; both are valid for this assertion.
    mockAnalyzeViaEngine1.mockResolvedValueOnce(fullShapedResponse({
      grade: 'D',
      confidence: 25,
      edge_pct: -12.1,
    }));

    const res = await request(app)
      .post('/api/analyze/prop')
      .send({
        player: 'Nikola Jokic',
        stat_type: 'points',
        line: 35.5,
        direction: 'over',
        book: 'draftkings',
      })
      .expect(200);

    expect(['D', 'F']).toContain(res.body.grade);
  });

  it('surfaces kill conditions through the adapter', async () => {
    // The 7e adapter translates engine1 factors into kill_conditions
    // entries. The legacy "blowout_risk" code is gone; the new code
    // set comes from FACTOR_TO_KILL_CONDITION in gradeAdapter.js.
    mockAnalyzeViaEngine1.mockResolvedValueOnce(fullShapedResponse({
      grade: 'C',
      confidence: 35,
      kill_conditions_triggered: [
        { code: 'TRAP', reason: 'Multiple trap signals firing.' },
      ],
    }));

    const res = await request(app)
      .post('/api/analyze/prop')
      .send({
        player: 'Nikola Jokic',
        stat_type: 'points',
        line: 24.5,
        direction: 'over',
        book: 'draftkings',
      })
      .expect(200);

    expect(Array.isArray(res.body.kill_conditions_triggered)).toBe(true);
    expect(res.body.kill_conditions_triggered.length).toBeGreaterThan(0);
    const first = res.body.kill_conditions_triggered[0];
    expect(typeof first.code).toBe('string');
    expect(typeof first.reason).toBe('string');
    expect(['C', 'D', 'F']).toContain(res.body.grade);
  });

  it('returns 400 for missing player field', async () => {
    const res = await request(app)
      .post('/api/analyze/prop')
      .send({ stat_type: 'points', line: 26.5, direction: 'over' })
      .expect(400);

    expect(res.body.error).toContain('player is required');
    // Validation runs before the engine — the helper must not be called.
    expect(mockAnalyzeViaEngine1).not.toHaveBeenCalled();
  });

  it('returns 400 for invalid stat_type', async () => {
    const res = await request(app)
      .post('/api/analyze/prop')
      .send({ player: 'Jokic', stat_type: 'invalid', line: 26.5, direction: 'over' })
      .expect(400);

    expect(res.body.error).toContain('Invalid stat_type');
  });

  it('returns 400 for missing direction', async () => {
    const res = await request(app)
      .post('/api/analyze/prop')
      .send({ player: 'Jokic', stat_type: 'points', line: 26.5 })
      .expect(400);

    expect(res.body.error).toContain('direction is required');
  });
});

describe('POST /api/analyze/batch', () => {
  it('processes multiple props and returns array', async () => {
    mockAnalyzeViaEngine1
      .mockResolvedValueOnce(fullShapedResponse({ stat_type: 'points', line: 26.5 }))
      .mockResolvedValueOnce(fullShapedResponse({ stat_type: 'rebounds', line: 12.5, grade: 'B' }));

    const res = await request(app)
      .post('/api/analyze/batch')
      .send({
        props: [
          { player: 'Nikola Jokic', stat_type: 'points', line: 26.5, direction: 'over', book: 'draftkings' },
          { player: 'Nikola Jokic', stat_type: 'rebounds', line: 12.5, direction: 'over', book: 'fanduel' },
        ],
      })
      .expect(200);

    expect(Array.isArray(res.body.results)).toBe(true);
    expect(res.body.results.length).toBe(2);
    for (const r of res.body.results) {
      expect(r.grade).toMatch(/^[ABCDF]$/);
      expect(typeof r.confidence).toBe('number');
      expect(r.reasoning.summary).toBeDefined();
    }
  });

  it('returns 400 for empty props array', async () => {
    const res = await request(app)
      .post('/api/analyze/batch')
      .send({ props: [] })
      .expect(400);

    expect(res.body.error).toContain('props array is required');
  });
});
