/**
 * F5SEO-005: AggregateOffer aggregates must derive from the pricingTiers array,
 * not from hardcoded values, so the schema stays in sync as tiers change.
 */
import { render } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import { WelcomeHead } from './WelcomeHead';

// Mock @inertiajs/react's Head so <script type="application/ld+json"> renders in JSDOM.
vi.mock('@inertiajs/react', async () => {
  const actual = await vi.importActual('@inertiajs/react');
  return {
    ...actual,
    Head: ({ children, title }: { children?: React.ReactNode; title?: string }) => (
      <>
        {title && <title>{title}</title>}
        {children}
      </>
    ),
  };
});

/** Parse the ld+json script blocks injected by <Head> into a queryable array. */
function extractJsonLd(container: HTMLElement): unknown[] {
  const scripts = container.querySelectorAll('script[type="application/ld+json"]');
  return Array.from(scripts).map((s) => JSON.parse(s.textContent ?? '{}'));
}

/**
 * Returns true when a schema node's @type field is — or includes — the given type.
 * TSEO-01: @type is now ['Product', 'SoftwareApplication'] so we must handle both
 * string and array values.
 */
function hasSchemaType(schema: Record<string, unknown>, type: string): boolean {
  const t = schema['@type'];
  if (typeof t === 'string') return t === type;
  if (Array.isArray(t)) return t.includes(type);
  return false;
}

/** Find the AggregateOffer node within any Product-co-typed schema block. */
function findAggregateOffer(schemas: unknown[]): Record<string, unknown> | null {
  for (const schema of schemas) {
    const s = schema as Record<string, unknown>;
    if (
      (hasSchemaType(s, 'Product') || hasSchemaType(s, 'SoftwareApplication')) &&
      s['offers'] &&
      typeof s['offers'] === 'object' &&
      (s['offers'] as Record<string, unknown>)['@type'] === 'AggregateOffer'
    ) {
      return s['offers'] as Record<string, unknown>;
    }
  }
  return null;
}

describe('WelcomeHead — AggregateOffer schema', () => {
  const baseProps = {
    appName: 'RankWizAI',
    appUrl: 'https://rankwizai.com',
    testimonials: [],
  };

  it('offerCount matches pricingTiers.length', () => {
    const { container } = render(
      <WelcomeHead
        {...baseProps}
        pricingTiers={[
          { name: 'Free', price: '$0' },
          { name: 'Pro', price: '$49' },
          { name: 'Team', price: '$99' },
        ]}
      />,
    );
    const schemas = extractJsonLd(container);
    const offer = findAggregateOffer(schemas);
    expect(offer).not.toBeNull();
    expect(offer?.['offerCount']).toBe('3');
  });

  it('offerCount updates automatically when a tier is removed', () => {
    const { container } = render(
      <WelcomeHead
        {...baseProps}
        pricingTiers={[
          { name: 'Free', price: '$0' },
          { name: 'Pro', price: '$49' },
        ]}
      />,
    );
    const schemas = extractJsonLd(container);
    const offer = findAggregateOffer(schemas);
    expect(offer?.['offerCount']).toBe('2');
  });

  it('highPrice is the max numeric price from pricingTiers', () => {
    const { container } = render(
      <WelcomeHead
        {...baseProps}
        pricingTiers={[
          { name: 'Free', price: '$0' },
          { name: 'Pro', price: '$49' },
          { name: 'Team', price: '$199' },
        ]}
      />,
    );
    const schemas = extractJsonLd(container);
    const offer = findAggregateOffer(schemas);
    expect(offer?.['highPrice']).toBe('199');
  });

  it('lowPrice is always "0" (free plan always present)', () => {
    const { container } = render(
      <WelcomeHead
        {...baseProps}
        pricingTiers={[
          { name: 'Free', price: '$0' },
          { name: 'Pro', price: '$49' },
        ]}
      />,
    );
    const schemas = extractJsonLd(container);
    const offer = findAggregateOffer(schemas);
    expect(offer?.['lowPrice']).toBe('0');
  });

  it('each tier maps to an individual Offer node with correct price', () => {
    const tiers = [
      { name: 'Free', price: '$0' },
      { name: 'Pro', price: '$49' },
    ];
    const { container } = render(<WelcomeHead {...baseProps} pricingTiers={tiers} />);
    const schemas = extractJsonLd(container);
    const offer = findAggregateOffer(schemas);
    const offers = offer?.['offers'] as Array<Record<string, unknown>>;
    expect(Array.isArray(offers)).toBe(true);
    expect(offers).toHaveLength(2);
    expect(offers[0]?.['name']).toBe('Free');
    expect(offers[1]?.['price']).toBe('49');
  });

  it('offerCount does not stay at a hardcoded 3 when a 4th tier is added', () => {
    const { container } = render(
      <WelcomeHead
        {...baseProps}
        pricingTiers={[
          { name: 'Free', price: '$0' },
          { name: 'Pro', price: '$49' },
          { name: 'Team', price: '$99' },
          { name: 'Enterprise', price: '$299' },
        ]}
      />,
    );
    const schemas = extractJsonLd(container);
    const offer = findAggregateOffer(schemas);
    expect(offer?.['offerCount']).toBe('4');
    expect(offer?.['highPrice']).toBe('299');
  });

  // TSEO-02: every child Offer must carry availability + priceValidUntil
  it('every child Offer includes availability https://schema.org/InStock', () => {
    const { container } = render(
      <WelcomeHead
        {...baseProps}
        pricingTiers={[
          { name: 'Free', price: '$0' },
          { name: 'Pro', price: '$49' },
        ]}
      />,
    );
    const schemas = extractJsonLd(container);
    const aggregateOffer = findAggregateOffer(schemas);
    const childOffers = aggregateOffer?.['offers'] as Array<Record<string, unknown>>;
    expect(Array.isArray(childOffers)).toBe(true);
    expect(childOffers.length).toBeGreaterThan(0);
    for (const offer of childOffers) {
      expect(offer['availability']).toBe('https://schema.org/InStock');
    }
  });

  it('every child Offer includes priceValidUntil as end-of-next-year ISO date', () => {
    const { container } = render(
      <WelcomeHead
        {...baseProps}
        pricingTiers={[
          { name: 'Free', price: '$0' },
          { name: 'Pro', price: '$49' },
        ]}
      />,
    );
    const schemas = extractJsonLd(container);
    const aggregateOffer = findAggregateOffer(schemas);
    const childOffers = aggregateOffer?.['offers'] as Array<Record<string, unknown>>;
    expect(Array.isArray(childOffers)).toBe(true);
    const expectedYear = new Date().getFullYear() + 1;
    for (const offer of childOffers) {
      expect(offer['priceValidUntil']).toBe(`${expectedYear}-12-31`);
    }
  });
});

// TSEO-01: Product co-type assertions — the money-page entity must be
// eligible for Google Product rich results.
describe('WelcomeHead — TSEO-01 Product co-type', () => {
  const baseProps = {
    appName: 'RankWizAI',
    appUrl: 'https://rankwizai.com',
    testimonials: [],
  };

  const pricingTiers = [
    { name: 'Free', price: '$0' },
    { name: 'Pro', price: '$49' },
  ];

  function findProductNode(schemas: unknown[]): Record<string, unknown> | null {
    for (const schema of schemas) {
      const s = schema as Record<string, unknown>;
      if (hasSchemaType(s, 'Product')) return s;
    }
    return null;
  }

  it('emits an @type array containing Product', () => {
    const { container } = render(<WelcomeHead {...baseProps} pricingTiers={pricingTiers} />);
    const schemas = extractJsonLd(container);
    const node = findProductNode(schemas);
    expect(node).not.toBeNull();
    expect(Array.isArray(node?.['@type'])).toBe(true);
    expect(node?.['@type']).toContain('Product');
  });

  it('co-types as both Product and SoftwareApplication', () => {
    const { container } = render(<WelcomeHead {...baseProps} pricingTiers={pricingTiers} />);
    const schemas = extractJsonLd(container);
    const node = findProductNode(schemas);
    expect(node?.['@type']).toContain('SoftwareApplication');
  });

  it('attaches offers to the Product-co-typed node', () => {
    const { container } = render(<WelcomeHead {...baseProps} pricingTiers={pricingTiers} />);
    const schemas = extractJsonLd(container);
    const node = findProductNode(schemas);
    expect(node?.['offers']).toBeDefined();
    expect((node?.['offers'] as Record<string, unknown>)?.['@type']).toBe('AggregateOffer');
  });

  it('attaches aggregateRating to the Product-co-typed node when testimonials qualify', () => {
    const qualifiedTestimonials = [
      { rating: 5, source: 'G2', source_url: 'https://g2.com' },
      { rating: 4, source: 'Capterra', source_url: 'https://capterra.com' },
      { rating: 5, source: 'ProductHunt', source_url: null },
      { rating: 4, source: 'Trustpilot', source_url: null },
      { rating: 5, source: 'Reddit', source_url: null },
    ];
    const { container } = render(
      <WelcomeHead
        {...baseProps}
        pricingTiers={pricingTiers}
        testimonials={qualifiedTestimonials}
      />,
    );
    const schemas = extractJsonLd(container);
    const node = findProductNode(schemas);
    const rating = node?.['aggregateRating'] as Record<string, unknown> | undefined;
    expect(rating).toBeDefined();
    expect(rating?.['@type']).toBe('AggregateRating');
    expect(typeof rating?.['ratingValue']).toBe('number');
    expect(typeof rating?.['reviewCount']).toBe('number');
    expect((rating?.['reviewCount'] as number) > 0).toBe(true);
  });

  it('does not emit aggregateRating when testimonials are empty (zero reviewCount guard)', () => {
    const { container } = render(
      <WelcomeHead {...baseProps} pricingTiers={pricingTiers} testimonials={[]} />,
    );
    const schemas = extractJsonLd(container);
    const node = findProductNode(schemas);
    expect(node?.['aggregateRating']).toBeUndefined();
  });
});

// TSEO-03: canonical Product @id invariant
// The WelcomeHead is the SOLE owner of the full Product+SoftwareApplication node.
// It must emit @id rooted at appUrl (never at appUrl/pricing or a sub-path).
describe('WelcomeHead — TSEO-03 canonical Product @id', () => {
  const baseProps = {
    appName: 'RankWizAI',
    appUrl: 'https://rankwiz.ai',
    testimonials: [],
  };
  const pricingTiers = [{ name: 'Free', price: '$0' }, { name: 'Pro', price: '$49' }];

  it('@id on the Product node is root-anchored (never sub-path/#product)', () => {
    const { container } = render(<WelcomeHead {...baseProps} pricingTiers={pricingTiers} />);
    const schemas = extractJsonLd(container);
    const node = schemas.find(
      (s) => hasSchemaType(s as Record<string, unknown>, 'Product'),
    ) as Record<string, unknown> | undefined;
    expect(node).not.toBeUndefined();
    // Must be the root fragment, not /pricing/#product or any sub-path
    expect(node?.['@id']).toBe('https://rankwiz.ai/#product');
  });

  it('exactly one Product-co-typed node exists in the head (no duplicates)', () => {
    const { container } = render(<WelcomeHead {...baseProps} pricingTiers={pricingTiers} />);
    const schemas = extractJsonLd(container);
    const productNodes = schemas.filter(
      (s) => hasSchemaType(s as Record<string, unknown>, 'Product'),
    );
    expect(productNodes).toHaveLength(1);
  });
});
