import { describe, expect, it } from 'vitest'

import { createGiteaClient, normalizeIssue } from './client.js'
import { GiteaApiError, type FetchLike, type GiteaConfig, type GiteaRequestInit } from './types.js'

const CONFIG: GiteaConfig = {
  baseUrl: 'https://gitea.stephenmann.io',
  token: 'tok_secret',
  owner: 'christian',
  repo: 'commitea',
}

/** Raw gitea issue JSON as the API returns it. */
const RAW_ISSUE = {
  number: 9,
  title: 'Deterministic scheduler',
  body: 'topo order → dated plan',
  state: 'open',
  labels: [{ name: 'est/5d' }, { name: 'p/1' }, { name: 'deadline/hard' }],
  milestone: { id: 7, title: 'P2 — Scheduler + Monte Carlo', due_on: '2026-09-01T00:00:00Z' },
  assignee: { login: 'christian' },
  assignees: [{ login: 'christian' }, { login: 'stephen' }],
  created_at: '2026-07-08T00:00:00Z',
  updated_at: '2026-07-08T01:00:00Z',
  closed_at: null,
  html_url: 'https://gitea.stephenmann.io/christian/commitea/issues/9',
}

/** A stub fetch that records calls and returns a canned 200 JSON body. */
function stubFetch(body: unknown, status = 200): {
  fetch: FetchLike
  calls: { url: string; init?: GiteaRequestInit }[]
} {
  const calls: { url: string; init?: GiteaRequestInit }[] = []
  const fetch: FetchLike = (url, init) => {
    calls.push({ url, init })
    return Promise.resolve({
      ok: status >= 200 && status < 300,
      status,
      json: () => Promise.resolve(body),
      text: () => Promise.resolve(typeof body === 'string' ? body : JSON.stringify(body)),
    })
  }
  return { fetch, calls }
}

describe('createGiteaClient.getIssue', () => {
  it('returns a normalized, typed issue from canned JSON', async () => {
    const { fetch } = stubFetch(RAW_ISSUE)
    const issue = await createGiteaClient(CONFIG, fetch).getIssue(9)

    expect(issue.number).toBe(9)
    expect(issue.title).toBe('Deterministic scheduler')
    expect(issue.state).toBe('open')
    expect(issue.labels).toEqual(['est/5d', 'p/1', 'deadline/hard'])
    expect(issue.url).toBe('https://gitea.stephenmann.io/christian/commitea/issues/9')
  })

  it('precomputes scheduler facts via extractLabelFacts', async () => {
    const { fetch } = stubFetch(RAW_ISSUE)
    const issue = await createGiteaClient(CONFIG, fetch).getIssue(9)

    expect(issue.facts.estimateDays).toBe(5)
    expect(issue.facts.priority).toBe(1)
    expect(issue.facts.hardDeadline).toBe(true)
  })

  it('maps the milestone ref and assignees', async () => {
    const { fetch } = stubFetch(RAW_ISSUE)
    const issue = await createGiteaClient(CONFIG, fetch).getIssue(9)

    expect(issue.milestone).toEqual({
      id: 7,
      title: 'P2 — Scheduler + Monte Carlo',
      dueOn: '2026-09-01T00:00:00Z',
    })
    expect(issue.assignee).toBe('christian')
    expect(issue.assignees).toEqual(['christian', 'stephen'])
  })

  it('hits the configured repo path with the token auth header', async () => {
    const { fetch, calls } = stubFetch(RAW_ISSUE)
    await createGiteaClient(CONFIG, fetch).getIssue(9)

    expect(calls).toHaveLength(1)
    expect(calls[0].url).toBe(
      'https://gitea.stephenmann.io/api/v1/repos/christian/commitea/issues/9',
    )
    expect(calls[0].init?.headers?.Authorization).toBe('token tok_secret')
  })

  it('tolerates a trailing slash on baseUrl', async () => {
    const { fetch, calls } = stubFetch(RAW_ISSUE)
    await createGiteaClient({ ...CONFIG, baseUrl: 'https://gitea.stephenmann.io/' }, fetch).getIssue(9)

    expect(calls[0].url).toBe(
      'https://gitea.stephenmann.io/api/v1/repos/christian/commitea/issues/9',
    )
  })

  it('throws GiteaApiError carrying status + body on a non-2xx response', async () => {
    const { fetch } = stubFetch('not found', 404)
    const client = createGiteaClient(CONFIG, fetch)

    await expect(client.getIssue(999)).rejects.toBeInstanceOf(GiteaApiError)
    await expect(client.getIssue(999)).rejects.toMatchObject({ status: 404, body: 'not found' })
  })
})

describe('createGiteaClient.listIssues', () => {
  /** Stub fetch that pages 50-at-a-time and records the paths it was asked for. */
  function pagedFetch(pages: unknown[][]): { fetch: FetchLike; paths: string[] } {
    const paths: string[] = []
    const fetch: FetchLike = (url) => {
      paths.push(url)
      const page = Number(/[?&]page=(\d+)/.exec(url)?.[1] ?? '1')
      const body = pages[page - 1] ?? []
      return Promise.resolve({
        ok: true,
        status: 200,
        json: () => Promise.resolve(body),
        text: () => Promise.resolve(''),
      })
    }
    return { fetch, paths }
  }

  it('walks every page until a short page ends it', async () => {
    const full = Array.from({ length: 50 }, (_, i) => ({ ...RAW_ISSUE, number: i + 1 }))
    const tail = [{ ...RAW_ISSUE, number: 51 }]
    const { fetch, paths } = pagedFetch([full, tail])

    const issues = await createGiteaClient(CONFIG, fetch).listIssues()
    expect(issues).toHaveLength(51)
    expect(paths).toHaveLength(2) // stopped after the short second page
    expect(paths[0]).toContain('/issues?type=issues&state=all&page=1&limit=50')
  })

  it('excludes pull requests even if the API returns them', async () => {
    const { fetch } = pagedFetch([
      [{ ...RAW_ISSUE, number: 1 }, { ...RAW_ISSUE, number: 2, pull_request: { url: 'x' } }],
    ])
    const issues = await createGiteaClient(CONFIG, fetch).listIssues()
    expect(issues.map((i) => i.number)).toEqual([1])
  })

  it('passes the state filter through', async () => {
    const { fetch, paths } = pagedFetch([[]])
    await createGiteaClient(CONFIG, fetch).listIssues({ state: 'open' })
    expect(paths[0]).toContain('state=open')
  })
})

describe('normalizeIssue', () => {
  it('defaults missing labels/assignees/body to empty and maps a closed state', () => {
    const issue = normalizeIssue({
      number: 1,
      title: 'bare issue',
      body: null,
      state: 'closed',
      labels: null,
      milestone: null,
      assignee: null,
      assignees: null,
      created_at: '2026-07-01T00:00:00Z',
      updated_at: '2026-07-02T00:00:00Z',
      closed_at: '2026-07-02T00:00:00Z',
      html_url: 'https://gitea.stephenmann.io/christian/commitea/issues/1',
    })

    expect(issue.state).toBe('closed')
    expect(issue.body).toBe('')
    expect(issue.labels).toEqual([])
    expect(issue.assignee).toBeNull()
    expect(issue.assignees).toEqual([])
    expect(issue.milestone).toBeNull()
    expect(issue.facts.estimateDays).toBeNull()
    expect(issue.closedAt).toBe('2026-07-02T00:00:00Z')
  })
})
