import { describe, expect, it, vi } from 'vitest';
import { getActiveRoute, postActiveRoute } from '../src/routes/active-context.js';
import { ACTIVE_CONTEXT_TTL_MS } from '../src/constants.js';

interface MockStore {
  current:
    | {
        projectId: string;
        fileName: string | null;
        ts: number;
      }
    | null;
}

function makeDeps(now = 1_000) {
  const store: MockStore = { current: null };
  // Annotated return type widens the mock so `.mockReturnValue(null)` is
  // allowed by the inferred Mock type later in the file.
  const getProject = vi.fn(
    (_db: unknown, id: string): { name?: string | null } | null | undefined => ({
      name: `Project ${id}`,
    }),
  );
  return {
    store,
    db: { fake: true },
    getProject,
    now: () => now,
  };
}

const EMPTY_INPUT = { body: {}, query: {}, params: {} };

describe('active context — POST /api/active', () => {
  it('clears the store when body.active === false', async () => {
    const deps = makeDeps();
    deps.store.current = { projectId: 'p1', fileName: 'a.html', ts: 1 };
    const parsed = postActiveRoute.parse({ ...EMPTY_INPUT, body: { active: false } });
    expect(parsed.ok).toBe(true);
    if (!parsed.ok) return;
    const out = await postActiveRoute.handle(parsed.value, deps);
    expect(out).toEqual({ ok: true, value: { active: false } });
    expect(deps.store.current).toBeNull();
  });

  it('rejects when projectId is missing', () => {
    const parsed = postActiveRoute.parse({ ...EMPTY_INPUT, body: {} });
    expect(parsed.ok).toBe(false);
    if (parsed.ok) return;
    expect(parsed.error.code).toBe('BAD_REQUEST');
    expect(parsed.error.message).toBe('projectId is required');
  });

  it('stores projectId + fileName + timestamp on success', async () => {
    const deps = makeDeps(5_000);
    const parsed = postActiveRoute.parse({
      ...EMPTY_INPUT,
      body: { projectId: 'p1', fileName: 'index.html' },
    });
    expect(parsed.ok).toBe(true);
    if (!parsed.ok) return;
    const out = await postActiveRoute.handle(parsed.value, deps);
    expect(out).toEqual({
      ok: true,
      value: { active: true, projectId: 'p1', fileName: 'index.html', ts: 5_000 },
    });
    expect(deps.store.current).toEqual({ projectId: 'p1', fileName: 'index.html', ts: 5_000 });
  });

  it('treats empty fileName as null', async () => {
    const deps = makeDeps(7_000);
    const parsed = postActiveRoute.parse({
      ...EMPTY_INPUT,
      body: { projectId: 'p1', fileName: '' },
    });
    expect(parsed.ok).toBe(true);
    if (!parsed.ok) return;
    const out = await postActiveRoute.handle(parsed.value, deps);
    expect(out.ok).toBe(true);
    if (!out.ok) return;
    expect(out.value).toMatchObject({ active: true, fileName: null });
  });
});

describe('active context — GET /api/active', () => {
  it('returns inactive when nothing is stored', async () => {
    const deps = makeDeps();
    const out = await getActiveRoute.handle(undefined, deps);
    expect(out).toEqual({ ok: true, value: { active: false } });
  });

  it('returns inactive and clears when TTL has expired', async () => {
    const deps = makeDeps(10_000 + ACTIVE_CONTEXT_TTL_MS);
    deps.store.current = { projectId: 'p1', fileName: null, ts: 9_000 };
    const out = await getActiveRoute.handle(undefined, deps);
    expect(out).toEqual({ ok: true, value: { active: false } });
    expect(deps.store.current).toBeNull();
  });

  it('returns active payload with project name + ageMs when fresh', async () => {
    const deps = makeDeps(2_500);
    deps.store.current = { projectId: 'p7', fileName: 'plan.md', ts: 2_000 };
    const out = await getActiveRoute.handle(undefined, deps);
    expect(out.ok).toBe(true);
    if (!out.ok) return;
    expect(out.value).toEqual({
      active: true,
      projectId: 'p7',
      projectName: 'Project p7',
      fileName: 'plan.md',
      ts: 2_000,
      ageMs: 500,
    });
    expect(deps.getProject).toHaveBeenCalledWith(deps.db, 'p7');
  });

  it('tolerates a missing project (projectName = null)', async () => {
    const deps = makeDeps(3_000);
    deps.getProject.mockReturnValue(null);
    deps.store.current = { projectId: 'p9', fileName: null, ts: 2_500 };
    const out = await getActiveRoute.handle(undefined, deps);
    expect(out.ok).toBe(true);
    if (!out.ok) return;
    expect(out.value).toMatchObject({ active: true, projectName: null });
  });
});
