import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import postgres from 'postgres';
import Fastify from 'fastify';
import { registerSettingsRoutes } from '../settings.js';
import type { Sql } from '../../db.js';

// P0 favorites hide-not-delete (multi-llama-swap-providers-model-favorites, P8):
// availability filtering is a CLIENT display concern — ModelPicker derives the
// visible Favorites section from settings ∩ live catalog. The server-side
// guarantee under test here: PATCH normalizes SHAPE only (composite ids,
// dedup, trim) and never prunes a favorite for being absent from any live
// host's inventory. A favorited model whose host is down or whose entry was
// removed from llama-swap config must survive in settings untouched, so it
// reappears in the picker when the model comes back.
//
// Skipped unless DATABASE_URL is set (tool_cost_stats.test.ts pattern). Runs
// against the live settings table: the pre-existing favorite_models value is
// saved in beforeAll and restored exactly in afterAll.

const DB_URL = process.env.DATABASE_URL;
const describeFn = DB_URL ? describe : describe.skip;

const FAVORITES_KEY = 'favorite_models';
// No llama-swap host serves this id; shape-valid composite ref.
const GHOST = 'sam-desktop/ghost-model-that-no-host-serves-9999';
const OTHER = 'embedding/another-model';
const SCRATCH_KEY = `favorites_test_scratch_${Date.now()}`;

describeFn('PATCH /api/settings favorite_models — hide-not-delete (P0 P8)', () => {
  let sql: ReturnType<typeof postgres>;
  let app: ReturnType<typeof Fastify>;
  let savedFavorites: unknown = null;
  let hadFavorites = false;

  beforeAll(async () => {
    if (!DB_URL) return;
    sql = postgres(DB_URL, { max: 2, idle_timeout: 5, connect_timeout: 5, onnotice: () => {} });

    // Create ONLY the settings table (mirrors schema.sql:217). Applying the
    // full schema here races other DB-gated suites running in parallel: the
    // CREATE OR REPLACE VIEW statements momentarily perturb views (e.g.
    // tool_cost_stats) that tool_cost_stats.test.ts is querying mid-run.
    await sql`CREATE TABLE IF NOT EXISTS settings (
      key TEXT PRIMARY KEY,
      value JSONB NOT NULL
    )`;

    // Preserve the operator's real favorites for exact restore in afterAll.
    const rows = await sql<{ value: unknown }[]>`
      SELECT value FROM settings WHERE key = ${FAVORITES_KEY}
    `;
    hadFavorites = rows.length > 0;
    savedFavorites = rows[0]?.value ?? null;

    app = Fastify();
    registerSettingsRoutes(app, sql as unknown as Sql);
    await app.ready();
  });

  afterAll(async () => {
    if (!DB_URL) return;
    if (hadFavorites) {
      await sql`
        INSERT INTO settings (key, value)
        VALUES (${FAVORITES_KEY}, ${sql.json(savedFavorites as never)})
        ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
      `;
    } else {
      await sql`DELETE FROM settings WHERE key = ${FAVORITES_KEY}`;
    }
    await sql`DELETE FROM settings WHERE key = ${SCRATCH_KEY}`;
    await app.close();
    await sql.end({ timeout: 5 });
  });

  it('persists a favorite no live host serves — shape normalization only, no availability pruning', async () => {
    const res = await app.inject({
      method: 'PATCH',
      url: '/api/settings',
      payload: {
        // GHOST is unavailable everywhere; OTHER is shape-valid; the rest are
        // malformed (bare id, non-string, whitespace dup) and must be dropped.
        [FAVORITES_KEY]: [GHOST, OTHER, 'bare-id-no-slash', 42, `  ${OTHER}  `],
      },
    });
    expect(res.statusCode).toBe(200);
    const body = res.json() as Record<string, unknown>;
    expect(body[FAVORITES_KEY]).toEqual([GHOST, OTHER]);
  });

  it('GET returns the unavailable favorite untouched', async () => {
    const res = await app.inject({ method: 'GET', url: '/api/settings' });
    expect(res.statusCode).toBe(200);
    const body = res.json() as Record<string, unknown>;
    expect(body[FAVORITES_KEY]).toEqual([GHOST, OTHER]);
  });

  it('unrelated settings writes leave favorites untouched', async () => {
    const res = await app.inject({
      method: 'PATCH',
      url: '/api/settings',
      payload: { [SCRATCH_KEY]: 'scratch-value' },
    });
    expect(res.statusCode).toBe(200);
    const body = res.json() as Record<string, unknown>;
    expect(body[FAVORITES_KEY]).toEqual([GHOST, OTHER]);
    expect(body[SCRATCH_KEY]).toBe('scratch-value');
  });

  it('removal is explicit-only: a user PATCH without the ghost removes it', async () => {
    const res = await app.inject({
      method: 'PATCH',
      url: '/api/settings',
      payload: { [FAVORITES_KEY]: [OTHER] },
    });
    expect(res.statusCode).toBe(200);
    const body = res.json() as Record<string, unknown>;
    expect(body[FAVORITES_KEY]).toEqual([OTHER]);
  });
});
