import { describe, it, expect } from 'vitest';
import { composePageMeta, detectSchemaType } from '../../seo/page-meta.js';
/** Pull the @graph array out of a composed JSON-LD result. */
function graphOf(jsonLd) {
    const g = jsonLd?.['@graph'];
    return Array.isArray(g) ? g : [];
}
/** Find the first graph node whose @type matches (string or array form). */
function nodeOfType(jsonLd, type) {
    return graphOf(jsonLd).find((n) => {
        const t = n['@type'];
        return t === type || (Array.isArray(t) && t.includes(type));
    });
}
function makeCollection(overrides = {}) {
    return {
        slug: 'pages',
        labels: { singular: 'Page', plural: 'Pages' },
        fields: {},
        ...overrides,
    };
}
function makeConfig(overrides = {}) {
    return {
        seo: {
            siteUrl: 'https://example.com',
            siteName: 'Example',
            ...overrides,
        },
    };
}
describe('detectSchemaType', () => {
    it('maps known collection slugs', () => {
        expect(detectSchemaType('posts')).toBe('BlogPosting');
        expect(detectSchemaType('blog')).toBe('BlogPosting');
        expect(detectSchemaType('articles')).toBe('Article');
        expect(detectSchemaType('news')).toBe('NewsArticle');
        expect(detectSchemaType('case-studies')).toBe('Article');
        expect(detectSchemaType('faq')).toBe('FAQPage');
        expect(detectSchemaType('services')).toBe('Service');
        expect(detectSchemaType('locations')).toBe('LocalBusiness');
        expect(detectSchemaType('jobs')).toBe('JobPosting');
        expect(detectSchemaType('team')).toBe('Person');
        expect(detectSchemaType('portfolios')).toBe('CreativeWork');
    });
    it('soft-matches via includes', () => {
        expect(detectSchemaType('blog-posts')).toBe('BlogPosting');
        expect(detectSchemaType('our-services')).toBe('Service');
    });
    it('falls back to WebPage for unknown slugs', () => {
        expect(detectSchemaType('xyz-unknown')).toBe('WebPage');
    });
});
describe('composePageMeta', () => {
    it('builds canonical URL from site URL + collection prefix + slug', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'posts', slug: 'hello-world', data: { title: 'Hello World' } },
            collection: makeCollection({ slug: 'posts', urlPrefix: 'blog', type: 'post' }),
            config: makeConfig(),
        });
        expect(result.url).toBe('https://example.com/blog/hello-world');
        expect(result.canonical).toBe('https://example.com/blog/hello-world');
    });
    it('treats slug "home" as the site root', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'home', data: { title: 'Home' } },
            collection: makeCollection({ slug: 'pages', urlPrefix: '', type: 'page' }),
            config: makeConfig(),
        });
        expect(result.url).toBe('https://example.com');
    });
    it('honors the plugin-seo canonicalUrl field name', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'posts',
                slug: 'hello',
                data: { title: 'Hello', canonicalUrl: 'https://example.com/canonical-target' },
            },
            collection: makeCollection({ slug: 'posts', urlPrefix: 'blog', type: 'post' }),
            config: makeConfig(),
        });
        expect(result.canonical).toBe('https://example.com/canonical-target');
    });
    it('honors lowercase noindex and plugin-seo robotsNoindex field names', () => {
        const lower = composePageMeta({
            doc: { id: '1', collection: 'posts', slug: 'a', data: { title: 'A', noindex: true } },
            collection: makeCollection({ slug: 'posts', urlPrefix: 'blog', type: 'post' }),
            config: makeConfig(),
        });
        expect(lower.meta['robots']).toContain('noindex');
        const plugin = composePageMeta({
            doc: { id: '2', collection: 'posts', slug: 'b', data: { title: 'B', robotsNoindex: true } },
            collection: makeCollection({ slug: 'posts', urlPrefix: 'blog', type: 'post' }),
            config: makeConfig(),
        });
        expect(plugin.meta['robots']).toContain('noindex');
    });
    it('prefers explicit pageSettings.metaTitle over data.title', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'pages',
                slug: 'about',
                data: { title: 'Plain Title' },
                pageSettings: { metaTitle: 'SEO Title' },
            },
            collection: makeCollection(),
            config: makeConfig(),
        });
        expect(result.title).toBe('SEO Title');
    });
    it('composes a connected @graph with a BlogPosting node, author + publisher', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'posts',
                slug: 'first-post',
                data: { title: 'First Post', metaDescription: 'Hello', authorName: 'Jane Doe' },
                publishedAt: '2026-01-01T00:00:00.000Z',
            },
            collection: makeCollection({ slug: 'posts', urlPrefix: 'blog', type: 'post' }),
            config: makeConfig({
                organization: { name: 'Example Inc', logo: 'https://example.com/logo.png' },
            }),
        });
        expect(result.jsonLd?.['@context']).toBe('https://schema.org');
        const article = nodeOfType(result.jsonLd, 'BlogPosting');
        expect(article).toMatchObject({
            '@type': 'BlogPosting',
            headline: 'First Post',
            datePublished: '2026-01-01T00:00:00.000Z',
            // dateModified falls back to publication when no later edit exists.
            dateModified: '2026-01-01T00:00:00.000Z',
            publisher: { '@id': 'https://example.com/#organization' },
        });
        // The connected graph stacks WebSite + Organization site nodes.
        expect(nodeOfType(result.jsonLd, 'WebSite')).toBeDefined();
        const org = nodeOfType(result.jsonLd, 'Organization');
        expect(org).toMatchObject({ name: 'Example Inc' });
        // A Person node is present and the article references it.
        const person = nodeOfType(result.jsonLd, 'Person');
        expect(person).toMatchObject({ name: 'Jane Doe' });
        expect(article?.author).toEqual({ '@id': person?.['@id'] });
    });
    it('emits article:published_time / article:modified_time OG tags for articles', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'posts',
                slug: 'p',
                data: { title: 'P' },
                publishedAt: '2026-01-01T00:00:00.000Z',
                updatedAt: '2026-02-01T00:00:00.000Z',
            },
            collection: makeCollection({ slug: 'posts', urlPrefix: 'blog', type: 'post' }),
            config: makeConfig(),
        });
        expect(result.metaHtml).toContain('article:published_time');
        expect(result.metaHtml).toContain('2026-01-01T00:00:00.000Z');
        expect(result.metaHtml).toContain('article:modified_time');
        expect(result.metaHtml).toContain('2026-02-01T00:00:00.000Z');
    });
    it('uses an editorial publishedDate when the DB publishedAt is absent', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'posts',
                slug: 'p',
                data: { title: 'P', schemaType: 'BlogPosting', publishedDate: '2025-03-10' },
            },
            collection: makeCollection({ slug: 'posts', urlPrefix: 'blog', type: 'post' }),
            config: makeConfig(),
        });
        const article = nodeOfType(result.jsonLd, 'BlogPosting');
        expect(article?.datePublished).toBe(new Date('2025-03-10').toISOString());
    });
    it('respects per-collection defaultSchemaType as a graph entity node', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'locations',
                slug: 'nyc',
                data: { title: 'NYC', phone: '555-0100' },
            },
            collection: makeCollection({
                slug: 'locations',
                seo: { defaultSchemaType: 'LocalBusiness' },
            }),
            config: makeConfig(),
        });
        const biz = nodeOfType(result.jsonLd, 'LocalBusiness');
        expect(biz).toMatchObject({ '@type': 'LocalBusiness', name: 'NYC', telephone: '555-0100' });
        // The WebPage node cross-links to the business entity.
        const webPage = nodeOfType(result.jsonLd, 'WebPage');
        expect(webPage?.mainEntity).toEqual({ '@id': biz?.['@id'] });
    });
    it('derives a BreadcrumbList from the URL path (Home → … → page)', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'posts', slug: 'hello', data: { title: 'Hello' } },
            collection: makeCollection({ slug: 'posts', urlPrefix: 'blog', type: 'post' }),
            config: makeConfig(),
        });
        const crumbs = nodeOfType(result.jsonLd, 'BreadcrumbList');
        expect(crumbs).toBeDefined();
        const items = crumbs?.itemListElement;
        expect(items.map((i) => i.name)).toEqual(['Home', 'Blog', 'Hello']);
        expect(items[0].item).toBe('https://example.com');
        expect(items[2].item).toBe('https://example.com/blog/hello');
    });
    it('does not emit a breadcrumb for the homepage', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'home', data: { title: 'Home' } },
            collection: makeCollection({ slug: 'pages', urlPrefix: '', type: 'page' }),
            config: makeConfig(),
        });
        expect(nodeOfType(result.jsonLd, 'BreadcrumbList')).toBeUndefined();
    });
    it('emits Organization sameAs from config', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'about', data: { title: 'About' } },
            collection: makeCollection(),
            config: makeConfig({
                organization: {
                    name: 'Example Inc',
                    sameAs: ['https://twitter.com/example', 'https://linkedin.com/company/example'],
                },
            }),
        });
        const org = nodeOfType(result.jsonLd, 'Organization');
        expect(org?.sameAs).toEqual([
            'https://twitter.com/example',
            'https://linkedin.com/company/example',
        ]);
    });
    it('promotes FAQ pairs to FAQPage mainEntity on the WebPage node', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'pages',
                slug: 'faq',
                data: {
                    title: 'FAQ',
                    faqItems: [{ question: 'Is it free?', answer: 'Yes.' }],
                },
            },
            collection: makeCollection({ slug: 'faq' }),
            config: makeConfig(),
        });
        const webPage = nodeOfType(result.jsonLd, 'FAQPage');
        expect(webPage).toBeDefined();
        const mainEntity = webPage?.mainEntity;
        expect(mainEntity[0]).toMatchObject({ '@type': 'Question', name: 'Is it free?' });
    });
    it('honors doc-level structuredData verbatim when set', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'pages',
                slug: 'special',
                data: { title: 'Special' },
                structuredData: { '@type': 'FAQPage', mainEntity: [] },
            },
            collection: makeCollection(),
            config: makeConfig(),
        });
        expect(result.jsonLd).toEqual({
            '@context': 'https://schema.org',
            '@type': 'FAQPage',
            mainEntity: [],
        });
    });
    it('inherits robots defaults from CollectionSEOConfig', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'drafts', slug: 'x', data: { title: 'X' } },
            collection: makeCollection({
                slug: 'drafts',
                seo: { defaultRobots: { noIndex: true } },
            }),
            config: makeConfig(),
        });
        expect(result.meta['robots']).toContain('noindex');
    });
    it('honors the robotsPolicy select field (regression: was ignored)', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'pages',
                slug: 'secret',
                data: { title: 'Secret', robotsPolicy: 'noindex-nofollow' },
            },
            collection: makeCollection(),
            config: makeConfig(),
        });
        expect(result.meta['robots']).toContain('noindex');
        expect(result.meta['robots']).toContain('nofollow');
    });
    it('forces noindex outside production when seo.robots.noIndexNonProduction is set', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'home', data: { title: 'Home' } },
            collection: makeCollection(),
            config: makeConfig({ robots: { noIndexNonProduction: true } }),
            environment: 'preview',
        });
        expect(result.meta['robots']).toContain('noindex');
    });
    it('does NOT force noindex in production even with noIndexNonProduction set', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'home', data: { title: 'Home' } },
            collection: makeCollection(),
            config: makeConfig({ robots: { noIndexNonProduction: true } }),
            environment: 'production',
        });
        expect(result.meta['robots'] ?? '').not.toContain('noindex');
    });
    it('emits og:image, og:url, og:title, twitter:card', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'pages',
                slug: 'home',
                data: { title: 'Hi', metaDescription: 'A page', ogImage: 'https://example.com/cover.png' },
            },
            collection: makeCollection(),
            config: makeConfig({ twitterHandle: '@example' }),
        });
        expect(result.meta['og:image']).toBe('https://example.com/cover.png');
        expect(result.meta['og:url']).toBe('https://example.com');
        expect(result.meta['og:title']).toBe('Hi');
        expect(result.meta['twitter:card']).toBe('summary_large_image');
        expect(result.meta['twitter:site']).toBe('@example');
    });
    it('auto-builds og:image URL pointing at the CMS /api/cms/og.png endpoint when none configured', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'about', data: { title: 'About Us' } },
            collection: makeCollection(),
            config: makeConfig(),
        });
        expect(result.meta['og:image']).toBe('https://example.com/api/cms/og.png?title=' + encodeURIComponent('About Us'));
    });
    it('falls back to the page hero/featured image (object or string) for og:image', () => {
        const fromObject = composePageMeta({
            doc: {
                id: '1',
                collection: 'pages',
                slug: 'about',
                data: { title: 'About', featuredImage: { url: 'https://cdn.test/hero.jpg' } },
            },
            collection: makeCollection(),
            config: makeConfig(),
        });
        expect(fromObject.meta['og:image']).toBe('https://cdn.test/hero.jpg');
        const fromString = composePageMeta({
            doc: {
                id: '2',
                collection: 'pages',
                slug: 'team',
                data: { title: 'Team', heroImage: 'https://cdn.test/team.png' },
            },
            collection: makeCollection(),
            config: makeConfig(),
        });
        expect(fromString.meta['og:image']).toBe('https://cdn.test/team.png');
    });
    it('emits the GEO key takeaway as Schema.org abstract and a description fallback', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'posts',
                slug: 'guide',
                data: { title: 'Guide', keyTakeaway: 'Set up the CMS in under five minutes.' },
                publishedAt: '2026-01-01T00:00:00.000Z',
            },
            collection: makeCollection({ slug: 'posts', urlPrefix: 'blog', type: 'post' }),
            config: makeConfig(),
        });
        // abstract appears on the article node in the @graph.
        const article = nodeOfType(result.jsonLd, 'BlogPosting');
        expect(article?.abstract).toBe('Set up the CMS in under five minutes.');
        // With no meta description/excerpt, the takeaway becomes the description.
        expect(result.description).toBe('Set up the CMS in under five minutes.');
        expect(result.meta['og:description']).toBe('Set up the CMS in under five minutes.');
    });
    it('does not let the key takeaway override an explicit meta description', () => {
        const result = composePageMeta({
            doc: {
                id: '1',
                collection: 'pages',
                slug: 'x',
                data: {
                    title: 'X',
                    metaDescription: 'Explicit description.',
                    keyTakeaway: 'Takeaway summary.',
                },
            },
            collection: makeCollection(),
            config: makeConfig(),
        });
        expect(result.description).toBe('Explicit description.');
    });
    it('passes hreflang alternates through to the head and Next metadata', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'about', data: { title: 'About' } },
            collection: makeCollection(),
            config: makeConfig(),
            alternates: [
                { hreflang: 'en', href: 'https://example.com/about' },
                { hreflang: 'fr', href: 'https://example.com/fr/a-propos' },
            ],
        });
        expect(result.metaHtml).toContain('<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />');
        const metadata = result.metadata;
        expect(metadata.alternates?.languages?.fr).toBe('https://example.com/fr/a-propos');
        expect(metadata.metadataBase).toBeInstanceOf(URL);
    });
    it('escapes < in JSON-LD HTML so it cannot break out of <script>', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'x', data: { title: '</script><img src=x>' } },
            collection: makeCollection(),
            config: makeConfig(),
        });
        // The wrapping `<script>...</script>` legitimately contains the closing
        // tag, so we strip it off and assert no closing `<` survives in the body.
        const body = result.jsonLdHtml
            .replace(/^<script type="application\/ld\+json">/, '')
            .replace(/<\/script>$/, '');
        expect(body).not.toContain('</script>');
        expect(body).not.toContain('<img');
        expect(body).toContain('\\u003c');
    });
    it('exposes Next.js-compatible metadata object', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'home', data: { title: 'Home' } },
            collection: makeCollection(),
            config: makeConfig(),
        });
        const m = result.metadata;
        expect(m.title).toBe('Home');
        expect(m.openGraph.siteName).toBe('Example');
        expect(m.alternates.canonical).toBe('https://example.com');
    });
    it('overrides siteUrl when input.siteUrl is provided (preview deploys)', () => {
        const result = composePageMeta({
            doc: { id: '1', collection: 'pages', slug: 'about', data: { title: 'About' } },
            collection: makeCollection(),
            config: makeConfig(),
            siteUrl: 'https://preview-abc.vercel.app',
        });
        expect(result.url).toBe('https://preview-abc.vercel.app/about');
    });
});
//# sourceMappingURL=page-meta.test.js.map