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

import { extractLabelFacts } from '../labels/label-schema.js'
import type { FetchLike, GiteaIssue } from '../gitea/types.js'
import { runAgentTurn } from './agent-loop.js'
import { REGINALD_SYSTEM, REGINALD_TOOLS } from './agent-tools.js'
import { createChatClient } from './chat-client.js'
import { buildProjectView, type ProjectSnapshot } from './query-project.js'

/**
 * Opt-in (COMMITEA_MODEL_LIVE=1). Drives the real chat client + agent loop
 * against a local OpenAI-compatible server (LM Studio on :1234 by default),
 * proving the model calls the tools and narrates the real result. The model is
 * whatever is loaded (via LM Studio's native API), so it never JIT-swaps.
 */
const LIVE = !!process.env.COMMITEA_MODEL_LIVE
const BASE = process.env.COMMITEA_MODEL_URL ?? 'http://localhost:1234/v1'
let MODEL = process.env.COMMITEA_MODEL_SMALL ?? ''

beforeAll(async () => {
  if (!LIVE || MODEL) return
  const root = BASE.replace(/\/v1\/?$/, '')
  const loaded = await fetch(`${root}/api/v0/models`)
    .then((r) => (r.ok ? (r.json() as Promise<{ data?: { id: string; state?: string; type?: string }[] }>) : null))
    .then((d) => d?.data?.find((m) => m.state === 'loaded' && m.type !== 'embeddings')?.id)
    .catch(() => undefined)
  MODEL = loaded ?? 'google/gemma-4-e4b'
})

function issue(over: Partial<GiteaIssue>): GiteaIssue {
  const labels = over.labels ?? []
  return {
    number: 1, title: '#1', body: '', state: 'open', labels, facts: extractLabelFacts(labels),
    milestone: null, assignee: null, assignees: [], createdAt: '2026-01-05T09:00:00Z',
    updatedAt: '2026-01-05T09:00:00Z', closedAt: null, url: '', ...over,
  }
}

const SNAP: ProjectSnapshot = {
  issues: [
    issue({ number: 2, title: 'ChangeSource interface + polling source', labels: ['est/3d', 'p/1'] }),
    issue({ number: 3, title: 'SQLite cache bootstrap', labels: ['est/2d', 'p/1'] }),
  ],
  timelines: {},
  deps: [{ issue: 3, dependsOn: 2 }],
}

describe('agent loop (live model)', () => {
  it.skipIf(!LIVE)(
    'calls query_project and narrates the real focus',
    async () => {
      const client = createChatClient({ baseUrl: BASE, model: MODEL }, globalThis.fetch as unknown as FetchLike)
      const turn = await runAgentTurn({
        complete: (m, t) => client.complete(m, t),
        messages: [
          { role: 'system', content: REGINALD_SYSTEM },
          { role: 'user', content: 'What should I work on right now?' },
        ],
        tools: REGINALD_TOOLS,
        execute: async (name, args) =>
          name === 'query_project'
            ? buildProjectView((args as { view: any }).view, (args as any).filters, SNAP, new Date())
            : { error: `unknown tool ${name}` },
      })

      // it consulted the project, then answered in prose about the real top item (#2)
      expect(turn.steps.some((s) => s.tool === 'query_project')).toBe(true)
      expect(turn.content.trim().length).toBeGreaterThan(0)
      expect(turn.content).toMatch(/#?2\b|ChangeSource/i)
    },
    60_000,
  )

  it.skipIf(!LIVE)(
    'records a standing instruction via record_directive',
    async () => {
      const client = createChatClient({ baseUrl: BASE, model: MODEL }, globalThis.fetch as unknown as FetchLike)
      const recorded: unknown[] = []
      const turn = await runAgentTurn({
        complete: (m, t) => client.complete(m, t),
        messages: [
          { role: 'system', content: REGINALD_SYSTEM },
          { role: 'user', content: 'Log this standing directive: pilots come first, everything else waits.' },
        ],
        tools: REGINALD_TOOLS,
        execute: async (name, args) => {
          if (name === 'record_directive') {
            recorded.push(args)
            return { recorded: { kind: (args as { kind?: string }).kind ?? 'note' } }
          }
          return name === 'query_project'
            ? buildProjectView((args as { view: any }).view, (args as any).filters, SNAP, new Date())
            : { error: `unknown tool ${name}` }
        },
      })

      // the model logged the directive rather than trying to apply it
      expect(turn.steps.some((s) => s.tool === 'record_directive')).toBe(true)
      expect(recorded.length).toBeGreaterThan(0)
    },
    60_000,
  )
})
