import Fastify from 'fastify';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const repoMock = {
  listBases: vi.fn(),
  createBase: vi.fn(),
  getBase: vi.fn(),
  updateBase: vi.fn(),
  deleteBase: vi.fn(),
  listSources: vi.fn(),
  createSource: vi.fn(),
  getSource: vi.fn(),
  updateSource: vi.fn(),
  deleteSource: vi.fn(),
  upsertChunks: vi.fn(),
  listChunks: vi.fn(),
  searchChunks: vi.fn(),
  getBaseStats: vi.fn(),
};

vi.mock('./repository.js', () => repoMock);

async function buildApp(payload?: { sub: string; productId: string; role?: string }) {
  const { knowledgeRoutes } = await import('./routes.js');
  const app = Fastify({ logger: false });
  if (payload) {
    app.addHook('onRequest', async req => {
      req.jwtPayload = payload;
    });
  }
  await app.register(knowledgeRoutes, { prefix: '/api' });
  return app;
}

describe('knowledgeRoutes', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('POST /knowledge/bases creates a knowledge base', async () => {
    repoMock.createBase.mockResolvedValue({ id: 'kb_1', name: 'Support KB' });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/knowledge/bases',
      payload: {
        name: 'Support KB',
        retrievalStrategy: 'hybrid',
      },
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.createBase).toHaveBeenCalledWith(
      expect.objectContaining({
        name: 'Support KB',
        retrievalStrategy: 'hybrid',
      })
    );
  });

  it('POST /knowledge/sources/:id/chunks stores retrieval-ready chunk manifests', async () => {
    repoMock.getSource.mockResolvedValue({
      id: 'ksrc_1',
      knowledgeBaseId: 'kb_1',
      productId: 'lysnrai',
    });
    repoMock.upsertChunks.mockResolvedValue([{ id: 'kb_1:ksrc_1:0' }]);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/knowledge/sources/ksrc_1/chunks?knowledgeBaseId=kb_1',
      payload: {
        sourceStatus: 'indexed',
        chunks: [
          {
            ordinal: 0,
            contentText: 'Restart the worker before retrying the incident pipeline.',
            embeddingStatus: 'ready',
            tags: ['incident'],
          },
        ],
      },
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.upsertChunks).toHaveBeenCalledWith([
      expect.objectContaining({
        knowledgeBaseId: 'kb_1',
        sourceId: 'ksrc_1',
        embeddingStatus: 'ready',
      }),
    ]);
    expect(repoMock.updateSource).toHaveBeenCalledWith('ksrc_1', 'kb_1', { status: 'indexed' });
  });

  it('POST /knowledge/bases/:id/retrieval-query returns scored matches', async () => {
    repoMock.listChunks.mockResolvedValue([
      {
        id: 'chunk_1',
        sourceId: 'ksrc_1',
        ordinal: 0,
        contentText: 'Restart the worker before retrying the incident pipeline.',
        citations: ['https://example.com/runbook#restart'],
        tags: ['worker', 'incident'],
      },
      {
        id: 'chunk_2',
        sourceId: 'ksrc_1',
        ordinal: 1,
        contentText: 'Billing retries are handled separately.',
        citations: [],
        tags: ['billing'],
      },
    ]);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/knowledge/bases/kb_1/retrieval-query',
      payload: {
        query: 'incident worker',
        limit: 5,
      },
    });

    expect(res.statusCode).toBe(200);
    expect(res.json().matches[0]).toMatchObject({
      chunkId: 'chunk_1',
      sourceId: 'ksrc_1',
    });
  });

  it('GET /knowledge/bases/:id/stats returns source and chunk counts', async () => {
    repoMock.getBase.mockResolvedValue({ id: 'kb_1', productId: 'lysnrai' });
    repoMock.getBaseStats.mockResolvedValue({
      knowledgeBaseId: 'kb_1',
      sourceCount: 3,
      chunkCount: 42,
      totalTokens: 8500,
      indexedSources: 2,
      pendingSources: 1,
      failedSources: 0,
    });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({ method: 'GET', url: '/api/knowledge/bases/kb_1/stats' });

    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.sourceCount).toBe(3);
    expect(body.chunkCount).toBe(42);
    expect(body.totalTokens).toBe(8500);
  });

  it('DELETE /knowledge/bases/:id deletes draft base', async () => {
    repoMock.getBase.mockResolvedValue({ id: 'kb_1', productId: 'lysnrai', status: 'draft' });
    repoMock.deleteBase.mockResolvedValue(undefined);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({ method: 'DELETE', url: '/api/knowledge/bases/kb_1' });

    expect(res.statusCode).toBe(200);
    expect(res.json()).toEqual({ deleted: true });
  });

  it('DELETE /knowledge/bases/:id rejects non-draft base', async () => {
    repoMock.getBase.mockResolvedValue({ id: 'kb_1', productId: 'lysnrai', status: 'active' });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({ method: 'DELETE', url: '/api/knowledge/bases/kb_1' });

    expect(res.statusCode).toBe(400);
    expect(repoMock.deleteBase).not.toHaveBeenCalled();
  });

  it('DELETE /knowledge/bases/:id/sources/:sourceId deletes a source', async () => {
    repoMock.getBase.mockResolvedValue({ id: 'kb_1', productId: 'lysnrai' });
    repoMock.getSource.mockResolvedValue({ id: 'ksrc_1', knowledgeBaseId: 'kb_1' });
    repoMock.deleteSource.mockResolvedValue(undefined);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'DELETE',
      url: '/api/knowledge/bases/kb_1/sources/ksrc_1',
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.deleteSource).toHaveBeenCalledWith('ksrc_1', 'kb_1');
  });

  it('POST /knowledge/bases/:id/search returns matching chunks', async () => {
    repoMock.searchChunks.mockResolvedValue([
      {
        id: 'chunk_1',
        sourceId: 'ksrc_1',
        ordinal: 0,
        contentText: 'Restart the worker before retrying.',
        tokenCount: 12,
        citations: [],
        tags: ['worker'],
      },
    ]);

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/knowledge/bases/kb_1/search',
      payload: { query: 'worker', limit: 5 },
    });

    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.count).toBe(1);
    expect(body.chunks[0].id).toBe('chunk_1');
  });
});
