/**
 * CI-R14-01 (d): Contract test — every page that imports GenerateBriefModal
 * forwards `serpKeyAvailable` so the scope-downgrade notice is reachable.
 *
 * Two complementary layers:
 *   A. Behavioral: render each host with serp_key_available=false and assert
 *      the downgrade notice text appears (proves the prop is actually threaded).
 *   B. Enumeration guard: scan the Pages tree for GenerateBriefModal importers
 *      and assert the discovered set equals the enumerated set here (makes the
 *      "host page silently drops the prop" class non-recurrable).
 *
 * RECURRENCE GUARD: if a new host imports GenerateBriefModal without being
 * registered in the `ENUMERATED_HOSTS` list, Layer B fails loudly with:
 *   "Expected [...] but received [...]"
 * The fix is: wire serpKeyAvailable in the new host, then add it here.
 */
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, sep } from 'node:path';

import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';

import CannibalizationShow from '@/Pages/Cannibalization/Show';
import FreshnessShow from '@/Pages/ContentIntelligence/Freshness/Show';
import TopicClusterShow from '@/Pages/ContentIntelligence/TopicClusters/Show';

// ---------------------------------------------------------------------------
// Shared Inertia + layout mocks (mirrors each host's own .test.tsx so the
// pages mount headlessly without a real Inertia provider).
// ---------------------------------------------------------------------------

vi.mock('@inertiajs/react', async () => {
  const actual = await vi.importActual('@inertiajs/react');
  return {
    ...actual,
    Head: ({ title }: { title: string }) => <title>{title}</title>,
    Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
      <a href={href}>{children}</a>
    ),
    useForm: () => ({
      data: { target_keyword: '', custom_instructions: '' },
      setData: vi.fn(),
      post: vi.fn(),
      processing: false,
      reset: vi.fn(),
      errors: {},
    }),
    usePage: () => ({
      props: {
        ai_defaults: { allowed_models: ['gpt-4o'] },
      },
    }),
  };
});

vi.mock('@/Layouts/DashboardLayout', () => ({
  default: ({ children }: { children: React.ReactNode }) => (
    <div data-testid="dashboard-layout">{children}</div>
  ),
}));

vi.mock('@/Components/ContentIntelligence/DecayPatternBadge', () => ({
  default: ({ pattern }: { pattern: string }) => (
    <span data-testid="decay-badge">{pattern}</span>
  ),
}));

vi.mock('@/Components/ContentIntelligence/FreshnessScoreCard', () => ({
  default: ({ score }: { score: number }) => (
    <div data-testid="freshness-score-card">{score}</div>
  ),
}));

vi.mock('@/Components/ui/badge', () => ({
  Badge: ({ children, variant }: { children: React.ReactNode; variant?: string }) => (
    <span data-testid="badge" data-variant={variant}>
      {children}
    </span>
  ),
}));

vi.mock('@/Components/ui/button', () => ({
  Button: ({
    children,
    onClick,
    asChild,
  }: {
    children: React.ReactNode;
    onClick?: () => void;
    asChild?: boolean;
  }) => (asChild ? <>{children}</> : <button onClick={onClick}>{children}</button>),
}));

vi.mock('@/Components/ui/dialog', () => ({
  // Render children unconditionally (ignores `open`) so downgrade text is always in DOM.
  Dialog: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  DialogDescription: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  DialogFooter: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));

vi.mock('@/Components/ui/empty-state', () => ({
  EmptyState: ({
    title,
    description,
    action,
    primaryAction,
  }: {
    title: string;
    description?: string;
    action?: React.ReactNode;
    primaryAction?: { label: string; href?: string; onClick?: () => void };
  }) => (
    <div data-testid="empty-state">
      <h3>{title}</h3>
      {description && <p>{description}</p>}
      {action}
      {primaryAction &&
        (primaryAction.href ? (
          <a href={primaryAction.href}>{primaryAction.label}</a>
        ) : (
          <button onClick={primaryAction.onClick}>{primaryAction.label}</button>
        ))}
    </div>
  ),
}));

vi.mock('@/Components/ui/input', () => ({
  Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
}));

vi.mock('@/Components/ui/label', () => ({
  Label: ({ children }: { children: React.ReactNode }) => <label>{children}</label>,
}));

vi.mock('@/Components/ui/loading-button', () => ({
  LoadingButton: ({ children }: { children: React.ReactNode }) => (
    <button>{children}</button>
  ),
}));

vi.mock('@/Components/ui/textarea', () => ({
  Textarea: (props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) => (
    <textarea {...props} />
  ),
}));

// CI-R16-02: additional mocks for Cannibalization/Show and other hosts.
// export both trackEvent (Freshness/TopicClusters) and trackProductEvent (Cannibalization).
vi.mock('@/lib/analytics', () => ({
  trackEvent: vi.fn(),
  trackProductEvent: vi.fn(),
}));

vi.mock('@/lib/event-catalog', () => ({
  CANNIBALIZATION_VIEWED: 'cannibalization_viewed',
  FRESHNESS_ANALYSIS_VIEWED: 'freshness_analysis_viewed',
  TOPIC_CLUSTERS_DETAIL_VIEWED: 'topic_clusters_detail_viewed',
}));

vi.mock('@/lib/format', () => ({
  formatDateOnly: (d: string) => d,
  formatNumber: (n: number) => String(n),
  formatPercent: (n: number) => `${n}%`,
  formatConfidenceScore: (n: number | null | undefined) =>
    n === null || n === undefined || !Number.isFinite(n) ? '—' : `${Math.round(n)}%`,
}));

vi.mock('@/hooks/useMutationButton', () => ({
  useMutationButton: (handler: (opts: Record<string, unknown>) => void) => ({
    processing: false,
    onClick: () => handler({}),
  }),
}));

vi.mock('@/Components/layout/DetailPageHeader', () => ({
  default: ({ title }: { title: string }) => <h1>{title}</h1>,
}));

// ---------------------------------------------------------------------------
// Minimal prop fixtures for each host page.
// Derived from each page's own .test.tsx to stay in sync.
// ---------------------------------------------------------------------------

function freshnessShowProps(overrides: { serp_key_available: boolean }) {
  return {
    site: {
      id: '1',
      name: 'Test Site',
      domain: 'https://test.com',
      has_gsc_connection: true,
      has_wp_connection: false,
    },
    recommendation: {
      id: 1,
      public_id: '01arfxocbc123456789abcd',
      page_url: 'https://test.com/old-article',
      action_type: 'light_refresh' as const,
      impact_score: 78,
      confidence_score: 65,
      effort_score: 30,
      urgency_score: 55,
      freshness_score: 42,
      title: 'Old Article Needs Update',
      reasoning: 'Traffic has declined over 6 months',
      evidence: {},
      status: 'active',
      created_at: '2026-01-10T00:00:00Z',
      top_lost_query: null,
    },
    decay_signal: {
      decay_pattern_type: 'gradual_decay' as const,
      baseline_7d_clicks: 150,
      baseline_28d_clicks: 600,
      baseline_90d_clicks: 1800,
      current_clicks: 80,
      decay_magnitude: 0.47,
      confidence_score: 75,
      freshness_score: 42,
      supporting_data: {},
      created_at: '2026-01-10T00:00:00Z',
    },
    decay_run: {
      id: 1,
      analysis_window_start: '2025-10-01',
      analysis_window_end: '2026-01-01',
      completed_at: '2026-01-02T00:00:00Z',
    },
    wp_post: null,
    ...overrides,
  };
}

function topicClusterShowProps(overrides: { serp_key_available: boolean }) {
  return {
    site: { id: '1', name: 'Test Site', domain: 'https://test.com' },
    cluster: {
      id: '01J8XVZQ2VFTKM9H6PNX0GKNBR',
      name: 'SEO Content Strategy',
      total_demand: 8000,
      coverage_percentage: 55,
      hub_page_url: '/seo-content',
      metadata: null,
    },
    members: [
      {
        id: 1,
        member_type: 'page',
        member_value: '/seo-guide',
        clicks: 500,
        impressions: 5000,
        position: 4.2,
        is_hub: true,
      },
    ],
    entities: [
      {
        id: 1,
        entity_text: 'Google Search Console',
        entity_type: 'tool',
        demand_volume: 1200,
        coverage_strength: 0.8,
        source: 'gsc',
      },
    ],
    gap_suggestions: [],
    ...overrides,
  };
}

/** CI-R16-02: minimal fixture for Cannibalization/Show. */
function cannibalizationShowProps(overrides: { serp_key_available: boolean }) {
  return {
    site: { id: '1', name: 'Test Site', domain: 'https://test.com', has_gsc_connection: true, has_wp_connection: false },
    cannibalizationRun: { id: 1, status: 'completed', completed_at: '2026-01-01T00:00:00Z' },
    case: {
      id: 1,
      query_cluster: 'seo strategy',
      competing_pages_count: 3,
      lead_volatility_score: 0.5,
      impact_score: 75,
      confidence_score: 80,
      primary_page_url: 'https://test.com/seo',
      status: 'open',
      lifecycle_status: 'active',
      dismissed_at: null,
      created_at: '2026-01-01T00:00:00Z',
      consolidation_status: null,
      consolidation_draft_id: null,
      pages: [],
      queries: [],
      feedback: [],
    },
    ...overrides,
  };
}

// ---------------------------------------------------------------------------
// The canonical host list — this is the single source of truth.
// If a new page imports GenerateBriefModal, Layer B will fail and direct the
// author to wire serpKeyAvailable AND register the page here.
// ---------------------------------------------------------------------------
const ENUMERATED_HOSTS = [
  'resources/js/Pages/Cannibalization/Show.tsx',
  'resources/js/Pages/ContentIntelligence/Freshness/Show.tsx',
  'resources/js/Pages/ContentIntelligence/TopicClusters/Show.tsx',
].sort();

// Downgrade notice text emitted by GenerateBriefModal when serpKeyAvailable=false.
const DEGRADE_NOTICE = 'Generic brief (no competitor data)';

// ---------------------------------------------------------------------------
// Layer A: Behavioral — each host surfaces the downgrade notice
// ---------------------------------------------------------------------------

describe('GenerateBriefModal host contract — serpKeyAvailable is forwarded', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.stubGlobal('route', vi.fn((...args: string[]) => `/mock-route/${args[0]}`));
  });

  it('Freshness/Show surfaces the no-SERP-key downgrade notice when serp_key_available=false', () => {
    render(<FreshnessShow {...freshnessShowProps({ serp_key_available: false })} />);
    // Dialog mock renders children unconditionally; downgrade text is in DOM.
    expect(screen.getAllByText(DEGRADE_NOTICE).length).toBeGreaterThan(0);
  });

  it('Freshness/Show does NOT show the downgrade notice when serp_key_available=true', () => {
    render(<FreshnessShow {...freshnessShowProps({ serp_key_available: true })} />);
    expect(screen.queryByText(DEGRADE_NOTICE)).toBeNull();
  });

  it('TopicClusters/Show surfaces the no-SERP-key downgrade notice when serp_key_available=false', () => {
    render(<TopicClusterShow {...topicClusterShowProps({ serp_key_available: false })} />);
    // Two GenerateBriefModal instances on this page — both must be wired to serpKeyAvailable.
    expect(screen.getAllByText(DEGRADE_NOTICE).length).toBe(2);
  });

  it('TopicClusters/Show does NOT show the downgrade notice when serp_key_available=true', () => {
    render(<TopicClusterShow {...topicClusterShowProps({ serp_key_available: true })} />);
    expect(screen.queryByText(DEGRADE_NOTICE)).toBeNull();
  });

  // CI-R16-02: Cannibalization/Show host tests
  it('Cannibalization/Show surfaces the no-SERP-key downgrade notice when serp_key_available=false', () => {
    render(<CannibalizationShow {...cannibalizationShowProps({ serp_key_available: false })} />);
    // Dialog mock renders children unconditionally; downgrade text is in DOM.
    expect(screen.getAllByText(DEGRADE_NOTICE).length).toBeGreaterThan(0);
  });

  it('Cannibalization/Show does NOT show the downgrade notice when serp_key_available=true', () => {
    render(<CannibalizationShow {...cannibalizationShowProps({ serp_key_available: true })} />);
    expect(screen.queryByText(DEGRADE_NOTICE)).toBeNull();
  });

  it('Cannibalization/Show renders the target-keyword input through the shared modal', () => {
    render(<CannibalizationShow {...cannibalizationShowProps({ serp_key_available: true })} />);
    // Dialog mock renders children unconditionally.
    // The shared GenerateBriefModal renders an input with id "brief-target-keyword".
    // Confirm the input exists — prefill (from query_cluster) is exercised by the PHP layer.
    expect(document.getElementById('brief-target-keyword')).toBeTruthy();
  });
});

// ---------------------------------------------------------------------------
// Layer B: Enumeration guard — the host list is complete AND each host wires
// the prop. A new host that imports the modal WITHOUT being registered here
// causes the first assertion to fail loudly.
// ---------------------------------------------------------------------------

describe('GenerateBriefModal host enumeration guard', () => {
  it('every page that imports GenerateBriefModal is covered by this contract test AND forwards serpKeyAvailable', () => {
    const pagesDir = join(process.cwd(), 'resources/js/Pages');

    function findHostPages(dir: string, acc: string[] = []): string[] {
      for (const entry of readdirSync(dir)) {
        const full = join(dir, entry);
        if (statSync(full).isDirectory()) {
          findHostPages(full, acc);
        } else if (/\.tsx$/.test(entry) && !/\.test\.tsx$/.test(entry)) {
          const src = readFileSync(full, 'utf8');
          if (/from ['"][^'"]*GenerateBriefModal['"]/.test(src)) {
            acc.push(full);
          }
        }
      }
      return acc;
    }

    const cwd = process.cwd();
    const discovered = findHostPages(pagesDir)
      // Normalize to forward-slash relative paths so the comparison is platform-independent.
      .map((p) => p.replace(cwd + sep, '').replaceAll(sep, '/'))
      .sort();

    // 1) Coverage: the discovered set must equal the enumerated host set.
    //    Adding a new host without registering it here → RED.
    expect(discovered).toEqual(ENUMERATED_HOSTS);

    // 2) Each host source must actually forward the prop to GenerateBriefModal.
    //    This is a cheap second net; Layer A is the real behavioral proof.
    for (const rel of discovered) {
      const src = readFileSync(join(process.cwd(), rel), 'utf8');
      expect(src, `${rel} must forward serpKeyAvailable to GenerateBriefModal`).toMatch(
        /serpKeyAvailable=\{/,
      );
    }
  });
});
