/**
 * test/http/statusline.test.ts — TDD tests for parseStatusLine (N-statusline-be).
 *
 * Acceptance: AC-B2.6 — full/missing/dirty/empty bodies; never throw; ≥80% coverage.
 * Security:   SEC-M7 — unknown + narrowing, string length limits.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { parseStatusLine } from '../../src/http/statusline.js'
import type { StatusTelemetry } from '../../src/types.js'

// Representative full Claude Code statusLine JSON body.
// Field mapping (R0 → StatusTelemetry): context_window.used_percentage→contextUsedPct,
// cost.total_cost_usd→costUsd, cost.total_lines_added/removed→linesAdded/Removed,
// model.display_name→model, effort.level→effort, pr→pr, rate_limits→rate.
const FULL_BODY = {
  context_window: { used_percentage: 42.5 },
  cost: { total_cost_usd: 0.05, total_lines_added: 100, total_lines_removed: 50 },
  model: { display_name: 'Claude 3.5 Sonnet' },
  effort: { level: 'normal' },
  pr: { number: 42, url: 'https://github.com/foo/bar/pull/42', reviewState: 'pending' },
  rate_limits: { fiveHourPct: 10, sevenDayPct: 20 },
}

const FIXED_NOW = 1_700_000_000_000

describe('parseStatusLine — null for non-objects', () => {
  it('returns null for null', () => {
    expect(parseStatusLine(null)).toBeNull()
  })

  it('returns null for undefined', () => {
    expect(parseStatusLine(undefined)).toBeNull()
  })

  it('returns null for a string', () => {
    expect(parseStatusLine('hello')).toBeNull()
  })

  it('returns null for a number', () => {
    expect(parseStatusLine(42)).toBeNull()
  })

  it('returns null for a boolean', () => {
    expect(parseStatusLine(true)).toBeNull()
  })

  it('returns null for an array (typeof is "object" but is not a plain object)', () => {
    expect(parseStatusLine([])).toBeNull()
    expect(parseStatusLine([1, 2, 3])).toBeNull()
  })
})

describe('parseStatusLine — full valid body', () => {
  beforeEach(() => {
    vi.spyOn(Date, 'now').mockReturnValue(FIXED_NOW)
  })
  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('maps all fields correctly from a fully-populated body', () => {
    const result = parseStatusLine(FULL_BODY)

    expect(result).not.toBeNull()
    const t = result as StatusTelemetry

    expect(t.contextUsedPct).toBe(42.5)
    expect(t.costUsd).toBe(0.05)
    expect(t.linesAdded).toBe(100)
    expect(t.linesRemoved).toBe(50)
    expect(t.model).toBe('Claude 3.5 Sonnet')
    expect(t.effort).toBe('normal')
    expect(t.pr).toEqual({ number: 42, url: 'https://github.com/foo/bar/pull/42', reviewState: 'pending' })
    expect(t.rate).toEqual({ fiveHourPct: 10, sevenDayPct: 20 })
    expect(t.at).toBe(FIXED_NOW)
  })
})

describe('parseStatusLine — empty/minimal body', () => {
  beforeEach(() => {
    vi.spyOn(Date, 'now').mockReturnValue(FIXED_NOW)
  })
  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('returns a StatusTelemetry with only at for an empty object', () => {
    const result = parseStatusLine({})

    expect(result).not.toBeNull()
    expect(result?.at).toBe(FIXED_NOW)
    expect(result?.contextUsedPct).toBeUndefined()
    expect(result?.costUsd).toBeUndefined()
    expect(result?.linesAdded).toBeUndefined()
    expect(result?.linesRemoved).toBeUndefined()
    expect(result?.model).toBeUndefined()
    expect(result?.effort).toBeUndefined()
    expect(result?.pr).toBeUndefined()
    expect(result?.rate).toBeUndefined()
  })
})

describe('parseStatusLine — at timestamp', () => {
  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('sets at to Date.now() on each call', () => {
    const t1 = 1000
    const t2 = 2000
    vi.spyOn(Date, 'now').mockReturnValueOnce(t1).mockReturnValueOnce(t2)

    expect(parseStatusLine({})?.at).toBe(t1)
    expect(parseStatusLine({})?.at).toBe(t2)
  })
})

describe('parseStatusLine — dirty number fields', () => {
  it('omits contextUsedPct when used_percentage is NaN', () => {
    expect(
      parseStatusLine({ context_window: { used_percentage: NaN } })?.contextUsedPct,
    ).toBeUndefined()
  })

  it('omits contextUsedPct when used_percentage is Infinity', () => {
    expect(
      parseStatusLine({ context_window: { used_percentage: Infinity } })?.contextUsedPct,
    ).toBeUndefined()
  })

  it('omits contextUsedPct when used_percentage is a string', () => {
    expect(
      parseStatusLine({ context_window: { used_percentage: '42' } })?.contextUsedPct,
    ).toBeUndefined()
  })

  it('omits costUsd when total_cost_usd is Infinity', () => {
    expect(parseStatusLine({ cost: { total_cost_usd: Infinity } })?.costUsd).toBeUndefined()
  })

  it('omits costUsd when total_cost_usd is NaN', () => {
    expect(parseStatusLine({ cost: { total_cost_usd: NaN } })?.costUsd).toBeUndefined()
  })

  it('omits linesAdded when total_lines_added is a string', () => {
    expect(parseStatusLine({ cost: { total_lines_added: 'bad' } })?.linesAdded).toBeUndefined()
  })

  it('omits linesRemoved when total_lines_removed is null', () => {
    expect(parseStatusLine({ cost: { total_lines_removed: null } })?.linesRemoved).toBeUndefined()
  })

  it('parses partial cost fields independently', () => {
    const result = parseStatusLine({ cost: { total_cost_usd: 1.5 } })

    expect(result?.costUsd).toBe(1.5)
    expect(result?.linesAdded).toBeUndefined()
    expect(result?.linesRemoved).toBeUndefined()
  })
})

describe('parseStatusLine — context_window not an object', () => {
  it('omits contextUsedPct when context_window is a string', () => {
    expect(parseStatusLine({ context_window: 'bad' })?.contextUsedPct).toBeUndefined()
  })

  it('omits contextUsedPct when context_window is null', () => {
    expect(parseStatusLine({ context_window: null })?.contextUsedPct).toBeUndefined()
  })

  it('omits contextUsedPct when context_window is a number', () => {
    expect(parseStatusLine({ context_window: 42 })?.contextUsedPct).toBeUndefined()
  })
})

describe('parseStatusLine — cost not an object', () => {
  it('omits all cost fields when cost is a string', () => {
    const result = parseStatusLine({ cost: 'bad' })

    expect(result?.costUsd).toBeUndefined()
    expect(result?.linesAdded).toBeUndefined()
    expect(result?.linesRemoved).toBeUndefined()
  })

  it('omits all cost fields when cost is null', () => {
    const result = parseStatusLine({ cost: null })

    expect(result?.costUsd).toBeUndefined()
    expect(result?.linesAdded).toBeUndefined()
    expect(result?.linesRemoved).toBeUndefined()
  })
})

describe('parseStatusLine — dirty string fields (SEC-M7 length caps)', () => {
  it('omits model when display_name exceeds 200 characters', () => {
    expect(
      parseStatusLine({ model: { display_name: 'a'.repeat(201) } })?.model,
    ).toBeUndefined()
  })

  it('accepts model at exactly 200 characters', () => {
    const model200 = 'a'.repeat(200)
    expect(parseStatusLine({ model: { display_name: model200 } })?.model).toBe(model200)
  })

  it('omits model when model field is not an object', () => {
    expect(parseStatusLine({ model: 'bad' })?.model).toBeUndefined()
    expect(parseStatusLine({ model: null })?.model).toBeUndefined()
    expect(parseStatusLine({ model: 42 })?.model).toBeUndefined()
  })

  it('omits effort when level is a number', () => {
    expect(parseStatusLine({ effort: { level: 42 } })?.effort).toBeUndefined()
  })

  it('omits effort when level exceeds 100 characters', () => {
    expect(
      parseStatusLine({ effort: { level: 'x'.repeat(101) } })?.effort,
    ).toBeUndefined()
  })

  it('accepts effort at exactly 100 characters', () => {
    const effort100 = 'e'.repeat(100)
    expect(parseStatusLine({ effort: { level: effort100 } })?.effort).toBe(effort100)
  })

  it('omits effort when effort field is not an object', () => {
    expect(parseStatusLine({ effort: 42 })?.effort).toBeUndefined()
    expect(parseStatusLine({ effort: null })?.effort).toBeUndefined()
    expect(parseStatusLine({ effort: [] })?.effort).toBeUndefined()
  })
})

describe('parseStatusLine — pr parsing', () => {
  it('parses pr with all fields including optional reviewState', () => {
    const result = parseStatusLine({
      pr: { number: 42, url: 'https://github.com/foo/bar/pull/42', reviewState: 'pending' },
    })

    expect(result?.pr).toEqual({
      number: 42,
      url: 'https://github.com/foo/bar/pull/42',
      reviewState: 'pending',
    })
  })

  it('parses pr without optional reviewState', () => {
    const result = parseStatusLine({ pr: { number: 5, url: 'https://github.com/pr/5' } })

    expect(result?.pr).toBeDefined()
    expect(result?.pr?.number).toBe(5)
    expect(result?.pr?.url).toBe('https://github.com/pr/5')
    expect(result?.pr?.reviewState).toBeUndefined()
  })

  it('omits pr when pr.number is missing', () => {
    expect(parseStatusLine({ pr: { url: 'https://example.com' } })?.pr).toBeUndefined()
  })

  it('omits pr when pr.url is missing', () => {
    expect(parseStatusLine({ pr: { number: 1 } })?.pr).toBeUndefined()
  })

  it('omits pr when both required fields are missing', () => {
    expect(parseStatusLine({ pr: {} })?.pr).toBeUndefined()
  })

  it('omits pr when pr is not an object', () => {
    expect(parseStatusLine({ pr: 'not-an-object' })?.pr).toBeUndefined()
    expect(parseStatusLine({ pr: null })?.pr).toBeUndefined()
    expect(parseStatusLine({ pr: 42 })?.pr).toBeUndefined()
  })

  it('omits pr when pr.url exceeds 2000 characters', () => {
    const longUrl = 'https://example.com/' + 'x'.repeat(1990)
    expect(parseStatusLine({ pr: { number: 1, url: longUrl } })?.pr).toBeUndefined()
  })

  it('omits pr.reviewState but keeps pr when reviewState exceeds 100 characters', () => {
    const result = parseStatusLine({
      pr: {
        number: 1,
        url: 'https://github.com/pr/1',
        reviewState: 'x'.repeat(101),
      },
    })

    expect(result?.pr).toBeDefined()
    expect(result?.pr?.number).toBe(1)
    expect(result?.pr?.url).toBe('https://github.com/pr/1')
    expect(result?.pr?.reviewState).toBeUndefined()
  })

  it('omits pr when pr.number is NaN', () => {
    expect(parseStatusLine({ pr: { number: NaN, url: 'https://github.com' } })?.pr).toBeUndefined()
  })
})

describe('parseStatusLine — rate_limits parsing', () => {
  it('parses rate_limits with both fields', () => {
    const result = parseStatusLine({ rate_limits: { fiveHourPct: 25, sevenDayPct: 50 } })

    expect(result?.rate).toEqual({ fiveHourPct: 25, sevenDayPct: 50 })
  })

  it('parses rate_limits with only fiveHourPct', () => {
    const result = parseStatusLine({ rate_limits: { fiveHourPct: 25 } })

    expect(result?.rate?.fiveHourPct).toBe(25)
    expect(result?.rate?.sevenDayPct).toBeUndefined()
  })

  it('parses rate_limits with only sevenDayPct', () => {
    const result = parseStatusLine({ rate_limits: { sevenDayPct: 75 } })

    expect(result?.rate?.fiveHourPct).toBeUndefined()
    expect(result?.rate?.sevenDayPct).toBe(75)
  })

  it('omits rate when rate_limits is not an object', () => {
    expect(parseStatusLine({ rate_limits: 'bad' })?.rate).toBeUndefined()
    expect(parseStatusLine({ rate_limits: null })?.rate).toBeUndefined()
    expect(parseStatusLine({ rate_limits: 42 })?.rate).toBeUndefined()
  })

  it('returns rate object with undefined sub-fields when rate_limits values are invalid', () => {
    const result = parseStatusLine({ rate_limits: { fiveHourPct: NaN, sevenDayPct: Infinity } })

    // rate_limits IS a valid object, so rate is returned; but both sub-fields are invalid
    expect(result?.rate).toBeDefined()
    expect(result?.rate?.fiveHourPct).toBeUndefined()
    expect(result?.rate?.sevenDayPct).toBeUndefined()
  })

  it('omits invalid sub-fields and keeps valid ones', () => {
    const result = parseStatusLine({
      rate_limits: { fiveHourPct: 'not-a-number', sevenDayPct: 80 },
    })

    expect(result?.rate?.fiveHourPct).toBeUndefined()
    expect(result?.rate?.sevenDayPct).toBe(80)
  })
})

describe('parseStatusLine — never throw (SEC-M7)', () => {
  it('never throws for wildly malformed bodies', () => {
    const malformed: unknown[] = [
      null,
      undefined,
      42,
      'string',
      true,
      [],
      { context_window: 'not-an-object' },
      { context_window: { used_percentage: {} } },
      { cost: null },
      { model: 42 },
      { effort: [] },
      { pr: 'not-an-object' },
      { pr: { number: 'bad', url: {} } },
      { rate_limits: 'bad' },
      { rate_limits: { fiveHourPct: Infinity, sevenDayPct: -Infinity } },
      { pr: { number: NaN, url: '' } },
      { context_window: { used_percentage: null } },
      { cost: { total_cost_usd: {}, total_lines_added: [], total_lines_removed: undefined } },
    ]

    for (const body of malformed) {
      expect(() => parseStatusLine(body)).not.toThrow()
    }
  })
})
