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

import React from 'react';

import OpportunitiesHub from './OpportunitiesHub';

/**
 * R6UXS-004: OpportunitiesHub is now a re-export of RecoverHub (the merged
 * Opportunities + Content hub). Tests updated to reflect the new cell structure.
 *
 * Previous tests asserted the old Opportunities-only cells ("Competing pages",
 * cannibalization, striking-distance). Those are now replaced by the recovery
 * lifecycle order: Action queue → Recommendations → Inventory → Batch AI →
 * Briefs → Topic clusters → Link opportunities.
 *
 *  1. Day-0 (no GSC) → HubDay0Empty hero; no editorial header readouts.
 *  2. Has GSC → editorial header + hairline cell-grid with recovery cells.
 *  3. Mount fires OPPORTUNITIES_HUB_VIEWED exactly once with { site_id }.
 */

const trackProductEventMock = vi.fn();

vi.mock('@/lib/analytics', () => ({
  trackProductEvent: (...args: unknown[]) => trackProductEventMock(...args),
}));

vi.mock('@inertiajs/react', async () => {
  const actual = await vi.importActual('@inertiajs/react');
  return {
    ...actual,
    Head: ({ title }: { title: string }) => <title>{title}</title>,
    Link: ({ href, children, ...rest }: { href: string; children: React.ReactNode }) => (
      <a href={href} {...rest}>
        {children}
      </a>
    ),
  };
});

vi.mock('@/Layouts/DashboardLayout', () => ({
  default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));

const baseSite = {
  id: '1',
  name: 'Example.com',
  domain: 'example.com',
};

const baseCounts = {
  pending_recommendations: 12,
  keyword_opportunities: 5,
  topic_clusters: 3,
  cannibalization_cases: 2,
  link_opportunities: 7,
  draftable_recommendations_count: 4,
  // R8UXB-004: freshness decay count
  freshness_recommendations: 0,
  // R6UXS-004: merged content counts (from OpportunitiesHubController after the merge)
  inventory: 42,
  briefs: 3,
  drafts_in_flight: 1,
};

describe('OpportunitiesHub — editorial layout', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.stubGlobal('route', (name: string, params?: number) =>
      params !== undefined ? `/${name}/${params}` : `/${name}`,
    );
  });

  it('renders Day-0 empty state when GSC is not connected', () => {
    render(
      <OpportunitiesHub
        site={{ ...baseSite, has_gsc: false, has_analysis: false }}
        counts={baseCounts}
      />,
    );

    expect(screen.getByText(/Find pages losing traffic on Example.com/i)).toBeInTheDocument();
    expect(
      screen.getByRole('link', { name: /Connect Google Search Console/i }),
    ).toBeInTheDocument();
    // Header readouts must NOT render on Day-0 (no counts to surface yet).
    expect(screen.queryByText(/Pending recs/i)).not.toBeInTheDocument();
  });

  it('renders the editorial header + hairline cell-grid when GSC is connected (R6UXS-004: RecoverHub cells)', () => {
    const { container } = render(
      <OpportunitiesHub
        site={{ ...baseSite, has_gsc: true, has_analysis: false }}
        counts={baseCounts}
      />,
    );

    // EditorialPageHeader rendered.
    expect(container.querySelector('[data-testid="page-header"]')).toBeInTheDocument();
    // Hairline cell-grid container present.
    expect(container.querySelector('[data-testid="hub-cell-grid"]')).toBeInTheDocument();

    // Header readouts show server-side counts.
    expect(screen.getByText(/Pending recs/i)).toBeInTheDocument();
    // RecoverHub cells: Action queue + Recommendations are analysis-gated,
    // so they render gated/locked — verify their titles still render.
    expect(screen.getByText('Action queue')).toBeInTheDocument();
    expect(screen.getByText('Recommendations')).toBeInTheDocument();
    // DUX-EOU-02: secondary cells (inventory etc.) are behind the "More tools" disclosure;
    // they do NOT render until the toggle is clicked, so this assertion is removed.
    // (The disclosure toggle is present for users to expand.)
    expect(screen.getByRole('button', { name: /more tools/i })).toBeInTheDocument();
  });

  it('routes the Triage Queue CTA to the Action Queue (opportunity-map)', () => {
    render(
      <OpportunitiesHub
        site={{ ...baseSite, has_gsc: true, has_analysis: true }}
        counts={{ ...baseCounts, inventory: 42, briefs: 3, drafts_in_flight: 1 }}
      />,
    );

    // When has_analysis=true, the action_queue cell is NOT gated — it renders as a link.
    // The cell routes to opportunity-map.index.
    const triageLink = screen
      .getAllByRole('link')
      .find((el) => (el as HTMLAnchorElement).href.includes('opportunity-map'));
    expect(triageLink).toBeDefined();
  });

  it('fires OPPORTUNITIES_HUB_VIEWED exactly once on mount with site_id', () => {
    render(
      <OpportunitiesHub
        site={{ ...baseSite, has_gsc: true, has_analysis: false }}
        counts={baseCounts}
      />,
    );

    expect(trackProductEventMock).toHaveBeenCalledTimes(1);
    expect(trackProductEventMock).toHaveBeenCalledWith('opportunities_hub_viewed', {
      site_id: '1',
    });
  });

  it('defaults counts to zero when the prop is omitted', () => {
    render(<OpportunitiesHub site={{ ...baseSite, has_gsc: true, has_analysis: false }} />);

    // Header still shows readouts, with zeros.
    expect(screen.getByText(/Pending recs/i)).toBeInTheDocument();
    expect(screen.getAllByText('0').length).toBeGreaterThan(0);
  });

  // R8UXB-004: Cannibalization and Freshness cards must render in the Detect section
  // DUX-EOU-02: these are secondary cells — expand the "More tools" disclosure first.
  it('renders Freshness and Cannibalization cards with counts when GSC + analysis active', async () => {
    const { default: userEvent } = await import('@testing-library/user-event');
    const user = userEvent.setup();
    const counts = {
      ...baseCounts,
      freshness_recommendations: 8,
      cannibalization_cases: 2,
    };
    render(
      <OpportunitiesHub
        site={{ ...baseSite, has_gsc: true, has_analysis: true }}
        counts={counts}
      />,
    );

    // DUX-EOU-02: expand secondary cells before asserting Freshness / Cannibalization.
    await user.click(screen.getByRole('button', { name: /more tools/i }));

    // Both cards must be present in the cell grid.
    expect(screen.getByText('Freshness')).toBeInTheDocument();
    expect(screen.getByText('Cannibalization')).toBeInTheDocument();

    // Count value for freshness renders (unique across the page).
    expect(screen.getAllByText('8').length).toBeGreaterThan(0);

    // Cards link to the correct routes.
    const freshnessLink = screen
      .getAllByRole('link')
      .find((el) => (el as HTMLAnchorElement).href.includes('freshness'));
    expect(freshnessLink).toBeDefined();

    const cannibalizationLink = screen
      .getAllByRole('link')
      .find((el) => (el as HTMLAnchorElement).href.includes('cannibalization'));
    expect(cannibalizationLink).toBeDefined();
  });

  it('gates Freshness and Cannibalization cards when has_analysis is false', async () => {
    const { default: userEvent } = await import('@testing-library/user-event');
    const user = userEvent.setup();
    render(
      <OpportunitiesHub
        site={{ ...baseSite, has_gsc: true, has_analysis: false }}
        counts={baseCounts}
      />,
    );

    // DUX-EOU-02: expand secondary cells before asserting Freshness / Cannibalization.
    await user.click(screen.getByRole('button', { name: /more tools/i }));

    // Both cards still render (gated) — they appear as titles.
    expect(screen.getByText('Freshness')).toBeInTheDocument();
    expect(screen.getByText('Cannibalization')).toBeInTheDocument();
  });

  // R16-PD-02: resume-run affordance in the RecoverHub header.
  describe('R16-PD-02: resume-run header CTA', () => {
    const inFlightBatch = {
      id: 'batch-ulid-01',
      status: 'processing',
      total_jobs: 8,
      completed_jobs: 4,
      pending_review_count: 3,
    };

    it('shows "Resume — N drafts awaiting review" as primary header action when in_flight_batch is set', () => {
      render(
        <OpportunitiesHub
          site={{ ...baseSite, has_gsc: true, has_analysis: true }}
          counts={baseCounts}
          in_flight_batch={inFlightBatch}
        />,
      );

      expect(screen.getByRole('link', { name: /resume — 3 drafts awaiting review/i })).toBeInTheDocument();
    });

    it('links the Resume CTA to review.index when in_flight_batch is set', () => {
      render(
        <OpportunitiesHub
          site={{ ...baseSite, has_gsc: true, has_analysis: true }}
          counts={baseCounts}
          in_flight_batch={inFlightBatch}
        />,
      );

      const resumeLink = screen.getByRole('link', { name: /resume — 3 drafts awaiting review/i });
      expect(resumeLink.getAttribute('href')).toContain('review.index');
    });

    it('shows "Start new" secondary button alongside Resume when in_flight_batch is set', () => {
      render(
        <OpportunitiesHub
          site={{ ...baseSite, has_gsc: true, has_analysis: true }}
          counts={baseCounts}
          in_flight_batch={inFlightBatch}
        />,
      );

      const startNewLink = screen.getByRole('link', { name: /start new/i });
      expect(startNewLink).toBeInTheDocument();
    });

    it('shows "Start Recovery Run" primary action when in_flight_batch is null', () => {
      render(
        <OpportunitiesHub
          site={{ ...baseSite, has_gsc: true, has_analysis: true }}
          counts={baseCounts}
          in_flight_batch={null}
        />,
      );

      expect(screen.getByRole('link', { name: /start recovery run/i })).toBeInTheDocument();
      expect(screen.queryByRole('link', { name: /resume/i })).not.toBeInTheDocument();
    });
  });
});
