import { describe, expect, it } from 'vitest';
import {
  createBibTeXEntry,
  createOpenAlexReferenceProvider,
  createSemanticScholarReferenceProvider,
  createZoteroReferenceProvider,
  dedupeReferenceRecords,
  matchReferenceToBibliography,
  parseBibTeX,
  suggestCitationKey,
  type ReferenceFetch,
  type ReferenceRecord,
} from '../index';

function jsonFetch(payload: unknown, status = 200): ReferenceFetch {
  return async () => new Response(JSON.stringify(payload), {
    status,
    headers: { 'content-type': 'application/json' },
  });
}

const paper: ReferenceRecord = {
  id: 'openalex:W1',
  provider: 'openalex',
  providerId: 'W1',
  title: 'Training Compute Optimal Language Models',
  authors: ['Jordan Hoffmann', 'Sebastian Borgeaud'],
  year: 2022,
  venue: 'NeurIPS',
  doi: '10.5555/example',
  citationCount: 42,
  openAccess: true,
};

describe('reference providers', () => {
  it('normalizes OpenAlex work metadata into the provider-neutral record', async () => {
    const provider = createOpenAlexReferenceProvider({
      fetch: jsonFetch({
        meta: { count: 1 },
        results: [{
          id: 'https://openalex.org/W1',
          display_name: paper.title,
          publication_year: 2022,
          doi: 'https://doi.org/10.5555/EXAMPLE',
          type: 'article',
          cited_by_count: 42,
          authorships: [{ author: { display_name: 'Jordan Hoffmann' } }],
          primary_location: { source: { display_name: 'NeurIPS' } },
          open_access: { is_oa: true },
        }],
      }),
    });
    const result = await provider.search({ query: 'compute optimal' });
    expect(result.total).toBe(1);
    expect(result.records[0]).toMatchObject({
      provider: 'openalex',
      doi: '10.5555/example',
      title: paper.title,
      citationCount: 42,
      openAccess: true,
    });
  });

  it('normalizes Semantic Scholar stable IDs without inventing metadata', async () => {
    const provider = createSemanticScholarReferenceProvider({
      fetch: jsonFetch({
        total: 1,
        data: [{
          paperId: 'S2-1',
          title: 'Scaling Laws',
          authors: [{ name: 'A. Researcher' }],
          year: 2020,
          externalIds: { ArXiv: '2001.00001' },
          citationCount: 9,
          openAccessPdf: null,
        }],
      }),
    });
    const result = await provider.search({ query: 'scaling laws' });
    expect(result.records[0]).toMatchObject({
      provider: 'semantic-scholar',
      arxivId: '2001.00001',
      citationCount: 9,
    });
    expect(result.records[0]?.doi).toBeUndefined();
  });


  it('filters by minimum citation count where provider counts are available', async () => {
    const provider = createOpenAlexReferenceProvider({
      fetch: jsonFetch({
        meta: { count: 2 },
        results: [
          { id: 'https://openalex.org/W1', display_name: 'Highly cited', cited_by_count: 25, authorships: [] },
          { id: 'https://openalex.org/W2', display_name: 'Low cited', cited_by_count: 2, authorships: [] },
        ],
      }),
    });
    const result = await provider.search({ query: 'cited', filters: { minCitationCount: 10 } });
    expect(result.records.map((record) => record.title)).toEqual(['Highly cited']);
  });

  it('uses Zotero v3 headers and keeps credentials out of request URLs', async () => {
    let seenUrl = '';
    let seenHeaders: Headers | undefined;
    const provider = createZoteroReferenceProvider({
      libraryId: '12345',
      apiKey: 'secret-key',
      fetch: async (input, init) => {
        seenUrl = String(input);
        seenHeaders = new Headers(init?.headers);
        return new Response(JSON.stringify([{ key: 'ABC', data: { title: 'Local Paper', creators: [], itemType: 'journalArticle' } }]), { status: 200 });
      },
    });
    await provider.search({ query: 'paper' });
    expect(seenUrl).not.toContain('secret-key');
    expect(seenHeaders?.get('Zotero-API-Version')).toBe('3');
    expect(seenHeaders?.get('Zotero-API-Key')).toBe('secret-key');
  });
});

describe('reference deduplication and BibTeX construction', () => {
  it('deduplicates DOI matches across providers and preserves richer metadata', () => {
    const merged = dedupeReferenceRecords([
      paper,
      { ...paper, id: 'crossref:1', provider: 'crossref', providerId: '1', citationCount: 100 },
    ]);
    expect(merged).toHaveLength(1);
    expect(merged[0]?.citationCount).toBe(100);
  });

  it('reuses existing bibliography entries in DOI-first confidence order', () => {
    const entry = parseBibTeX('@article{hoffmann2022training, title={Different title}, doi={10.5555/example}, year={2022}}').entries[0]!;
    const match = matchReferenceToBibliography(paper, [entry]);
    expect(match?.entry.key).toBe('hoffmann2022training');
    expect(match?.confidence).toBe('doi');
  });

  it('constructs only known BibTeX fields and chooses a collision-free key', () => {
    const key = suggestCitationKey(paper, new Set(['hoffmann2022training']));
    expect(key).not.toBe('hoffmann2022training');
    const bib = createBibTeXEntry(paper, key);
    expect(bib).toContain(`@article{${key},`);
    expect(bib).toContain('doi = {10.5555/example}');
    expect(bib).not.toContain('abstract =');
  });

  it('treats provider metadata as plain text when constructing BibTeX', () => {
    const hostile: ReferenceRecord = {
      ...paper,
      title: String.raw`Safe } title, note={\input{evil}} %`,
      authors: [String.raw`A. Researcher & B. Researcher`],
      url: String.raw`javascript:alert(1)`,
    };
    const bib = createBibTeXEntry(hostile, 'safe2026entry');
    expect(bib).not.toContain('note={\\input{evil}}');
    expect(bib).not.toContain('url = {javascript:');
    expect(bib).toContain('\\textbackslash{}input');
    expect(bib).toContain('\\&');
  });

  it('rejects unsafe bibliography keys instead of writing malformed source', () => {
    expect(() => createBibTeXEntry(paper, 'bad}, note={evil')).toThrow(/Invalid bibliography key/);
  });
});
