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

import {
  softwareApplicationBase,
  productIdReference,
  breadcrumbListSchema,
  webPageSchema,
  articleSchema,
  faqPageSchema,
  buildOffer,
} from './schema';

describe('softwareApplicationBase', () => {
  it('returns applicationCategory SEO for structured data', () => {
    const result = softwareApplicationBase({
      name: 'RankWizAI',
      url: 'https://rankwizai.com',
      description: 'GSC-driven content recovery for WordPress',
      image: 'https://rankwizai.com/og/og-home-v2.png',
    });

    expect(result.applicationCategory).toBe('SEO');
  });

  // TSEO-01: co-type must include both Product and SoftwareApplication
  it('returns @type array containing Product and SoftwareApplication', () => {
    const result = softwareApplicationBase({
      name: 'RankWizAI',
      url: 'https://rankwizai.com',
      description: 'Test',
      image: 'https://rankwizai.com/img.png',
    });

    expect(Array.isArray(result['@type'])).toBe(true);
    expect(result['@type']).toContain('Product');
    expect(result['@type']).toContain('SoftwareApplication');
  });

  // TSEO-01: @id must use #product fragment (TSEO-03 compatible placeholder)
  it('sets @id to the #product fragment', () => {
    const result = softwareApplicationBase({
      name: 'RankWizAI',
      url: 'https://rankwizai.com',
      description: 'Test',
      image: 'https://rankwizai.com/img.png',
    });

    expect(result['@id']).toBe('https://rankwizai.com/#product');
  });

  it('includes screenshot when provided', () => {
    const result = softwareApplicationBase({
      name: 'RankWizAI',
      url: 'https://rankwizai.com',
      description: 'Test',
      image: 'https://rankwizai.com/img.png',
      screenshot: 'https://rankwizai.com/og/og-features-v2.png',
    });

    expect(result.screenshot).toEqual({
      '@type': 'ImageObject',
      url: 'https://rankwizai.com/og/og-features-v2.png',
    });
  });

  it('omits screenshot when not provided', () => {
    const result = softwareApplicationBase({
      name: 'RankWizAI',
      url: 'https://rankwizai.com',
      description: 'Test',
      image: 'https://rankwizai.com/img.png',
    });

    expect(result.screenshot).toBeUndefined();
  });

  it('sets operatingSystem to Web', () => {
    const result = softwareApplicationBase({
      name: 'RankWizAI',
      url: 'https://rankwizai.com',
      description: 'Test',
      image: 'https://rankwizai.com/img.png',
    });

    expect(result.operatingSystem).toBe('Web');
  });

  // TSEO-03: canonicalAppUrl overrides @id so page-scoped urls never pollute the entity graph
  it('uses canonicalAppUrl for @id when provided, ignoring the page url', () => {
    const result = softwareApplicationBase({
      name: 'RankWizAI',
      url: 'https://rankwiz.ai/pricing',
      description: 'Test',
      image: 'https://rankwiz.ai/img.png',
      canonicalAppUrl: 'https://rankwiz.ai',
    });

    // @id must always be root-anchored, never /pricing/#product
    expect(result['@id']).toBe('https://rankwiz.ai/#product');
  });

  it('falls back to url-based @id when canonicalAppUrl is not provided (backward compat)', () => {
    const result = softwareApplicationBase({
      name: 'RankWizAI',
      url: 'https://rankwiz.ai',
      description: 'Test',
      image: 'https://rankwiz.ai/img.png',
    });

    expect(result['@id']).toBe('https://rankwiz.ai/#product');
  });

  // TSEO-03: trailing slash on canonicalAppUrl must not produce double-slash
  it('strips trailing slash from canonicalAppUrl before appending /#product', () => {
    const result = softwareApplicationBase({
      name: 'RankWizAI',
      url: 'https://rankwiz.ai/pricing',
      description: 'Test',
      image: 'https://rankwiz.ai/img.png',
      canonicalAppUrl: 'https://rankwiz.ai/',
    });

    expect(result['@id']).toBe('https://rankwiz.ai/#product');
  });
});

// TSEO-03: productIdReference — bare @id reference node for non-canonical pages
describe('productIdReference', () => {
  it('returns an object with only @id set to the canonical product node', () => {
    const ref = productIdReference('https://rankwiz.ai');
    expect(ref).toEqual({ '@id': 'https://rankwiz.ai/#product' });
  });

  it('strips trailing slash from appUrl before appending /#product', () => {
    const ref = productIdReference('https://rankwiz.ai/');
    expect(ref).toEqual({ '@id': 'https://rankwiz.ai/#product' });
  });

  it('reference node has no @type, name, or offers keys', () => {
    const ref = productIdReference('https://rankwiz.ai');
    expect(ref).not.toHaveProperty('@type');
    expect(ref).not.toHaveProperty('name');
    expect(ref).not.toHaveProperty('offers');
  });
});

describe('breadcrumbListSchema', () => {
  it('creates a BreadcrumbList with correct item positions', () => {
    const result = breadcrumbListSchema([
      { name: 'Home', url: 'https://rankwizai.com' },
      { name: 'Features', url: 'https://rankwizai.com/features' },
    ]);

    expect(result['@type']).toBe('BreadcrumbList');
    expect(result.itemListElement).toHaveLength(2);
    expect(result.itemListElement[0].position).toBe(1);
    expect(result.itemListElement[1].position).toBe(2);
  });
});

describe('webPageSchema', () => {
  it('creates a WebPage schema with name, description, and url', () => {
    const result = webPageSchema({
      name: 'RankWizAI Features',
      description: 'Content recovery features',
      url: 'https://rankwizai.com/features',
    });

    expect(result['@type']).toBe('WebPage');
    expect(result.name).toBe('RankWizAI Features');
  });
});

// R6SEO-008: articleSchema — E-E-A-T author block, publisher @id, wordCount, keywords, isPartOf
describe('articleSchema', () => {
  const baseOptions = {
    headline: 'How to Recover from a Google Algorithm Update',
    description: 'A step-by-step guide to diagnosing and recovering from Google updates.',
    url: 'https://rankwizai.com/blog/google-algorithm-recovery',
    datePublished: '2026-01-15',
    dateModified: '2026-06-01',
  };

  // SD006: @type must be BlogPosting (not Article) to match the page microdata
  it('returns @type BlogPosting and @context schema.org', () => {
    const result = articleSchema(baseOptions);
    expect(result['@type']).toBe('BlogPosting');
    expect(result['@context']).toBe('https://schema.org');
  });

  it('populates mainEntityOfPage with WebPage @id pointing to the article url', () => {
    const result = articleSchema(baseOptions);
    expect(result.mainEntityOfPage).toEqual({
      '@type': 'WebPage',
      '@id': baseOptions.url,
    });
  });

  it('includes author Person block with name and url when provided', () => {
    const result = articleSchema({
      ...baseOptions,
      author: { name: 'Jane Smith', url: 'https://rankwizai.com/authors/jane-smith' },
    });
    expect(result.author).toMatchObject({
      '@type': 'Person',
      name: 'Jane Smith',
      url: 'https://rankwizai.com/authors/jane-smith',
    });
  });

  it('includes author sameAs array for E-E-A-T when provided', () => {
    const result = articleSchema({
      ...baseOptions,
      author: {
        name: 'Jane Smith',
        sameAs: ['https://linkedin.com/in/janesmith', 'https://twitter.com/janesmith'],
      },
    });
    expect((result.author as Record<string, unknown>)?.sameAs).toEqual([
      'https://linkedin.com/in/janesmith',
      'https://twitter.com/janesmith',
    ]);
  });

  it('omits author sameAs when array is empty', () => {
    const result = articleSchema({
      ...baseOptions,
      author: { name: 'Jane Smith', sameAs: [] },
    });
    expect((result.author as Record<string, unknown>)?.sameAs).toBeUndefined();
  });

  it('includes author jobTitle for E-E-A-T when provided', () => {
    const result = articleSchema({
      ...baseOptions,
      author: { name: 'Jane Smith', jobTitle: 'SEO Strategist' },
    });
    expect((result.author as Record<string, unknown>)?.jobTitle).toBe('SEO Strategist');
  });

  it('includes author description for E-E-A-T when provided', () => {
    const result = articleSchema({
      ...baseOptions,
      author: { name: 'Jane Smith', description: 'SEO expert with 10 years experience.' },
    });
    expect((result.author as Record<string, unknown>)?.description).toBe(
      'SEO expert with 10 years experience.',
    );
  });

  it('wraps author image as ImageObject when provided', () => {
    const result = articleSchema({
      ...baseOptions,
      author: { name: 'Jane Smith', image: 'https://rankwizai.com/authors/jane-avatar.jpg' },
    });
    expect((result.author as Record<string, unknown>)?.image).toEqual({
      '@type': 'ImageObject',
      url: 'https://rankwizai.com/authors/jane-avatar.jpg',
    });
  });

  it('omits author block entirely when author is not provided', () => {
    const result = articleSchema(baseOptions);
    expect(result.author).toBeUndefined();
  });

  it('includes publisher Organization block with name when provided', () => {
    const result = articleSchema({
      ...baseOptions,
      publisher: { name: 'RankWizAI' },
    });
    expect(result.publisher).toMatchObject({
      '@type': 'Organization',
      name: 'RankWizAI',
    });
  });

  it('sets publisher @id for entity linking when publisher.id is provided', () => {
    const result = articleSchema({
      ...baseOptions,
      publisher: { name: 'RankWizAI', id: 'https://rankwizai.com' },
    });
    expect((result.publisher as Record<string, unknown>)?.['@id']).toBe('https://rankwizai.com');
  });

  it('omits publisher @id when publisher.id is not provided', () => {
    const result = articleSchema({
      ...baseOptions,
      publisher: { name: 'RankWizAI' },
    });
    expect((result.publisher as Record<string, unknown>)?.['@id']).toBeUndefined();
  });

  it('wraps publisher logo as ImageObject when provided', () => {
    const result = articleSchema({
      ...baseOptions,
      publisher: { name: 'RankWizAI', logo: 'https://rankwizai.com/icon-512.png' },
    });
    expect((result.publisher as Record<string, unknown>)?.logo).toEqual({
      '@type': 'ImageObject',
      url: 'https://rankwizai.com/icon-512.png',
    });
  });

  it('accepts image as a plain string URL', () => {
    const result = articleSchema({
      ...baseOptions,
      image: 'https://rankwizai.com/og/blog-post.png',
    });
    expect(result.image).toBe('https://rankwizai.com/og/blog-post.png');
  });

  it('wraps image as ImageObject with width/height when provided as object', () => {
    const result = articleSchema({
      ...baseOptions,
      image: { url: 'https://rankwizai.com/og/blog-post.png', width: 1200, height: 630 },
    });
    expect(result.image).toEqual({
      '@type': 'ImageObject',
      url: 'https://rankwizai.com/og/blog-post.png',
      width: 1200,
      height: 630,
    });
  });

  it('omits image width/height from ImageObject when not provided', () => {
    const result = articleSchema({
      ...baseOptions,
      image: { url: 'https://rankwizai.com/og/blog-post.png' },
    });
    const img = result.image as Record<string, unknown>;
    expect(img.width).toBeUndefined();
    expect(img.height).toBeUndefined();
  });

  it('includes wordCount when provided', () => {
    const result = articleSchema({ ...baseOptions, wordCount: 1500 });
    expect(result.wordCount).toBe(1500);
  });

  it('omits wordCount when not provided', () => {
    const result = articleSchema(baseOptions);
    expect(result.wordCount).toBeUndefined();
  });

  it('includes keywords when provided', () => {
    const result = articleSchema({ ...baseOptions, keywords: 'google algorithm, seo recovery' });
    expect(result.keywords).toBe('google algorithm, seo recovery');
  });

  it('omits keywords when not provided', () => {
    const result = articleSchema(baseOptions);
    expect(result.keywords).toBeUndefined();
  });

  it('includes isPartOf as CreativeWorkSeries when provided', () => {
    const result = articleSchema({
      ...baseOptions,
      isPartOf: { name: 'Algorithm Recovery Guide', url: 'https://rankwizai.com/learn/algorithm-recovery-guide' },
    });
    expect(result.isPartOf).toEqual({
      '@type': 'CreativeWorkSeries',
      name: 'Algorithm Recovery Guide',
      url: 'https://rankwizai.com/learn/algorithm-recovery-guide',
    });
  });

  it('omits isPartOf when not provided', () => {
    const result = articleSchema(baseOptions);
    expect(result.isPartOf).toBeUndefined();
  });

  // SD007: publisher @id must be the canonical Organization node
  it('sets publisher @id to the canonical Organization node URL when provided', () => {
    const result = articleSchema({
      ...baseOptions,
      publisher: { name: 'RankWizAI', id: 'https://rankwiz.ai/#organization' },
    });
    expect((result.publisher as Record<string, unknown>)?.['@id']).toBe(
      'https://rankwiz.ai/#organization',
    );
  });

  // SD013: author Person @id derived from author_url
  it('sets author Person @id to the full URL when author.url is absolute', () => {
    const result = articleSchema({
      ...baseOptions,
      author: { name: 'Sood', url: 'https://rankwiz.ai/authors/sood' },
    });
    expect((result.author as Record<string, unknown>)?.['@id']).toBe(
      'https://rankwiz.ai/authors/sood',
    );
  });

  it('sets author Person @id by prepending the site origin when author.url is relative', () => {
    const result = articleSchema({
      ...baseOptions,
      author: { name: 'Sood', url: '/authors/sood' },
    });
    expect((result.author as Record<string, unknown>)?.['@id']).toBe(
      'https://rankwiz.ai/authors/sood',
    );
  });

  it('omits author Person @id when author.url is not provided', () => {
    const result = articleSchema({
      ...baseOptions,
      author: { name: 'Sood' },
    });
    expect((result.author as Record<string, unknown>)?.['@id']).toBeUndefined();
  });

  // SD015: speakable property
  it('includes speakable SpeakableSpecification with correct cssSelectors', () => {
    const result = articleSchema(baseOptions);
    expect(result.speakable).toEqual({
      '@type': 'SpeakableSpecification',
      cssSelector: ['h1', '.article-bluf', 'h2'],
    });
  });

  // BLOG-DATEMODIFIED-CLAMP: dateModified must never precede datePublished in emitted JSON-LD.
  // ~22 posts have last_reviewed before published_at; without the clamp their BlogPosting schema
  // emits an invalid dateModified that Google Rich Results / schema.org validation flags as an error.
  it('clamps dateModified to datePublished when dateModified precedes datePublished', () => {
    const result = articleSchema({
      ...baseOptions,
      datePublished: '2026-06-25T09:00:00+00:00',
      dateModified: '2026-06-19',
    });
    // Clamp: dateModified must equal datePublished, not the earlier reviewed date
    expect(result.dateModified).toBe('2026-06-25T09:00:00+00:00');
  });

  it('preserves dateModified when it is later than datePublished', () => {
    const result = articleSchema({
      ...baseOptions,
      datePublished: '2026-01-15',
      dateModified: '2026-06-01',
    });
    // No clamp: dateModified is after datePublished — preserve as-is
    expect(result.dateModified).toBe('2026-06-01');
  });

  it('preserves dateModified when it equals datePublished', () => {
    const result = articleSchema({
      ...baseOptions,
      datePublished: '2026-06-22',
      dateModified: '2026-06-22',
    });
    // Equal: clamp is a no-op
    expect(result.dateModified).toBe('2026-06-22');
  });

  it('clamps dateModified to datePublished when same calendar day but dateModified is midnight and datePublished has a later time', () => {
    // In production: published_at carries time (e.g. "2026-06-25T09:00:00.000000Z"),
    // last_reviewed is mapped to startOfDay midnight ("2026-06-25T00:00:00.000000Z").
    // new Date("2026-06-25T00:00:00Z") < new Date("2026-06-25T09:00:00Z") → true → clamp fires.
    // Semantically correct: dateModified = datePublished for unreviewed-after-publish content.
    const result = articleSchema({
      ...baseOptions,
      datePublished: '2026-06-25T09:00:00+00:00',
      dateModified: '2026-06-25T00:00:00+00:00',
    });
    expect(result.dateModified).toBe('2026-06-25T09:00:00+00:00');
  });
});

// TSEO-02: buildOffer helper — availability + priceValidUntil
describe('buildOffer', () => {
  it('returns @type Offer', () => {
    const offer = buildOffer({ name: 'Pro', price: '49', priceCurrency: 'USD' });
    expect(offer['@type']).toBe('Offer');
  });

  it('includes availability as https://schema.org/InStock', () => {
    const offer = buildOffer({ name: 'Pro', price: '49', priceCurrency: 'USD' });
    expect(offer.availability).toBe('https://schema.org/InStock');
  });

  it('includes priceValidUntil as end-of-next-year ISO date', () => {
    const offer = buildOffer({ name: 'Pro', price: '49', priceCurrency: 'USD' });
    // Pin to the exact expected value so this test never becomes a time-bomb on Dec 31 of next year.
    const expectedYear = new Date().getFullYear() + 1;
    expect(offer.priceValidUntil).toBe(`${expectedYear}-12-31`);
  });

  it('passes through name, price, and priceCurrency unchanged', () => {
    const offer = buildOffer({ name: 'Free', price: '0', priceCurrency: 'USD' });
    expect(offer.name).toBe('Free');
    expect(offer.price).toBe('0');
    expect(offer.priceCurrency).toBe('USD');
  });

  it('includes optional description when provided', () => {
    const offer = buildOffer({
      name: 'Pro',
      price: '49',
      priceCurrency: 'USD',
      description: 'For growing sites',
    });
    expect(offer.description).toBe('For growing sites');
  });

  it('omits description when not provided', () => {
    const offer = buildOffer({ name: 'Pro', price: '49', priceCurrency: 'USD' });
    expect(offer.description).toBeUndefined();
  });

  it('sets availability InStock for a free (price 0) offer', () => {
    const offer = buildOffer({ name: 'Free', price: '0', priceCurrency: 'USD' });
    expect(offer.availability).toBe('https://schema.org/InStock');
  });
});

// SD005: faqPageSchema
describe('faqPageSchema', () => {
  it('returns @type FAQPage with @context schema.org', () => {
    const result = faqPageSchema([{ question: 'Q1?', answer: 'A1.' }]);
    expect(result['@type']).toBe('FAQPage');
    expect(result['@context']).toBe('https://schema.org');
  });

  it('maps 3 Q/A pairs to 3 mainEntity Question entries', () => {
    const pairs = [
      { question: 'What is SEO?', answer: 'Search engine optimisation.' },
      { question: 'Why does it matter?', answer: 'It drives organic traffic.' },
      { question: 'How long does it take?', answer: 'Usually 3–6 months.' },
    ];
    const result = faqPageSchema(pairs);
    expect(result.mainEntity).toHaveLength(3);
    expect(result.mainEntity[0]).toEqual({
      '@type': 'Question',
      name: 'What is SEO?',
      acceptedAnswer: { '@type': 'Answer', text: 'Search engine optimisation.' },
    });
    expect(result.mainEntity[2]).toMatchObject({
      '@type': 'Question',
      name: 'How long does it take?',
    });
  });

  it('returns empty mainEntity array when called with zero pairs', () => {
    const result = faqPageSchema([]);
    expect(result.mainEntity).toHaveLength(0);
  });
});
