/**
 * RecoveryHero unit tests (ROI-ONE-NUMBER).
 *
 * Invariants under test:
 *   1. Headline driven by measured_clicks_gained (gains-only), never total_clicks_lift.
 *   2. showNetLine gate: shown iff Math.round(net) !== Math.round(gains), hidden when equal.
 *   3. Empty/no-lift state when measured_clicks_gained <= 0.
 *   4. Revenue branch ($) when revenue_estimate > 0 and gains > 0.
 *   5. DOM-order: RecoveryHero must appear BEFORE DailyActionHero / RecoveryRunHero / GscMetricsPanel.
 *   6. Header readout demoted to "View ROI →" (no "+N clicks" numeric text in header).
 *
 * Do NOT assert 80 (net) as any headline — it represents losers included and is always wrong.
 */
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import { RecoveryHero } from './RecoveryHero';

// ── Minimal route mock ───────────────────────────────────────────────────────
vi.mock('@/test/setup', () => ({}));

// ── Shared test factory ──────────────────────────────────────────────────────
function makeTraffic(overrides: Partial<{
  measured_clicks_gained: number;
  total_clicks_lift: number;
  pages_improved: number;
  total_recommendations_tracked: number;
  site_id: string;
  spark_28d: Array<{ day: number; cumulative_clicks_lift: number }>;
  revenue_estimate: number | null;
  revenue_estimate_label: string | null;
  roi_per_click_value: number | null;
}> = {}) {
  return {
    measured_clicks_gained: 120,
    total_clicks_lift: 120,
    pages_improved: 3,
    total_recommendations_tracked: 5,
    site_id: '01J0RANKWZRBN0000000000001',
    spark_28d: [],
    revenue_estimate: null,
    revenue_estimate_label: null,
    roi_per_click_value: null,
    ...overrides,
  };
}

// ── 1. Gains-only headline (ROI-ONE-NUMBER) ──────────────────────────────────
describe('RecoveryHero — gains-only headline (ROI-ONE-NUMBER)', () => {
  it('shows measured_clicks_gained (120) not total_clicks_lift (80) when they diverge', () => {
    // Winners: +80 +40 = 120 gains. Loser: -40. Net = 80.
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          total_clicks_lift: 80,
        })}
      />,
    );

    // Headline must show +120 (gains-only).
    expect(screen.getByText(/\+120/)).toBeInTheDocument();
    // "80" must NOT appear as the primary big number — it's the wrong net figure.
    // The net line is permitted to show 80 in the muted secondary, but the
    // main heading must not resolve to a standalone 80.
    const headlineEl = screen.getByText(/\+120/);
    expect(headlineEl).toBeInTheDocument();
  });

  it('does NOT show "+80" as the headline when gains are 120 (Do NOT bless 80)', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          total_clicks_lift: 80,
        })}
      />,
    );

    // The card must not render a standalone "+80" as a headline figure.
    // (The net secondary line is allowed to mention "80" but only as
    // "Net change (incl. losses): 80" — not as a signed headline.)
    const allEls = screen.queryAllByText(/\+80/);
    // None of these elements should be the big headline (font-data text-3xl).
    allEls.forEach((el) => {
      expect(el).not.toHaveClass('font-data');
    });
  });
});

// ── 2. showNetLine gate ──────────────────────────────────────────────────────
describe('RecoveryHero — showNetLine gate (mirrors RoiSummaryCard:101-102)', () => {
  it('shows muted "Net change (incl. losses)" secondary line when net !== gains', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          total_clicks_lift: 80,
        })}
      />,
    );

    expect(screen.getByText(/Net change \(incl\. losses\)/i)).toBeInTheDocument();
  });

  it('hides the net secondary line when net === gains (no losers)', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          total_clicks_lift: 120,
        })}
      />,
    );

    expect(screen.queryByText(/Net change \(incl\. losses\)/i)).not.toBeInTheDocument();
  });

  it('hides the net secondary line when rounded values are equal (floating-point tolerance)', () => {
    // E.g. gains=120.4 net=120.6 → both round to 120 → no net line.
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          total_clicks_lift: 120,
        })}
      />,
    );

    expect(screen.queryByText(/Net change/i)).not.toBeInTheDocument();
  });

  it('hides the net secondary line when gains is 0 (no-lift state)', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 0,
          total_clicks_lift: -40,
        })}
      />,
    );

    // showNetLine = hasLift && ... ; hasLift is false when gains <= 0
    expect(screen.queryByText(/Net change/i)).not.toBeInTheDocument();
  });
});

// ── 3. Empty / no-lift state ─────────────────────────────────────────────────
describe('RecoveryHero — empty / no-lift state', () => {
  it('renders encouraging in-progress state when measured_clicks_gained is 0 but recs are tracked', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 0,
          total_clicks_lift: 0,
          total_recommendations_tracked: 3,
        })}
      />,
    );

    // Eyebrow shows progress label, not success label.
    expect(screen.getByText('Recovery in progress')).toBeInTheDocument();
    // Headline is non-numeric "measuring" copy — no "0 clicks".
    expect(screen.getByText(/Fixes applied — measuring impact/i)).toBeInTheDocument();
    // Sub-copy names the count and sets the expectation.
    expect(screen.getByText(/recovery typically takes 4–8 weeks/i)).toBeInTheDocument();
    // Must not render "+0" or "0 clicks" — fabricated success claim.
    expect(screen.queryByText(/\+0/)).not.toBeInTheDocument();
    expect(screen.queryByText(/0 clicks/i)).not.toBeInTheDocument();
  });

  it('does not show the $/click nudge when gains are 0', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 0,
          total_clicks_lift: 0,
        })}
      />,
    );

    expect(screen.queryByText(/Set your \$\/click/i)).not.toBeInTheDocument();
  });
});

// ── 4. Revenue branch ────────────────────────────────────────────────────────
describe('RecoveryHero — revenue headline (DASH-ROI-REVENUE)', () => {
  it('shows dollar estimate and "+N clicks" context line when revenue_estimate > 0', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          total_clicks_lift: 120,
          revenue_estimate: 60,
          revenue_estimate_label: 'at your $0.50/click — your estimate',
        })}
      />,
    );

    // Revenue headline (e.g. "$60 recovered")
    expect(screen.getByTestId('recovery-hero-revenue-headline')).toBeInTheDocument();
    // Context line shows gains-only click count — never net.
    const contextLine = screen.getByTestId('recovery-hero-clicks-context');
    expect(contextLine.textContent).toMatch(/\+120/);
  });

  it('shows $/click nudge when gains > 0 but no revenue_estimate', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          revenue_estimate: null,
          revenue_estimate_label: null,
        })}
      />,
    );

    expect(screen.getByTestId('recovery-hero-cpc-nudge')).toBeInTheDocument();
    expect(screen.getByText(/Set your \$\/click/i)).toBeInTheDocument();
  });

  it('does NOT show revenue headline when revenue_estimate is 0 (not a win)', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 0,
          total_clicks_lift: 0,
          revenue_estimate: 0,
          revenue_estimate_label: 'at your $0.50/click — your estimate',
        })}
      />,
    );

    expect(screen.queryByTestId('recovery-hero-revenue-headline')).not.toBeInTheDocument();
  });
});

// ── 5. Eyebrow label — conditional on lift state ─────────────────────────────
describe('RecoveryHero — eyebrow label', () => {
  it('shows "Proven traffic recovered" eyebrow when gains > 0', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
        })}
      />,
    );

    expect(screen.getByText('Proven traffic recovered')).toBeInTheDocument();
  });

  it('shows "Recovery in progress" eyebrow when gains <= 0', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 0,
          total_recommendations_tracked: 2,
        })}
      />,
    );

    expect(screen.getByText('Recovery in progress')).toBeInTheDocument();
    expect(screen.queryByText('Proven traffic recovered')).not.toBeInTheDocument();
  });
});

// ── R22-DASH-ROI-CPC: Nested-anchor fix + inline rate form ───────────────────
describe('RecoveryHero — R22-DASH-ROI-CPC (nested anchor fix + inline rate form)', () => {
  it('card wrapper is a <div>, NOT an <a> — no outer anchor element', () => {
    const { container } = render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
        })}
      />,
    );

    // The outermost element of the rendered hero should not be an anchor.
    // Previously the whole card was <Link href=sites.roi.index> (an <a>).
    // R22 restructure: the card is a plain <div>.
    const outerDiv = container.firstChild as HTMLElement;
    // The mb-6 wrapper div contains the card div — neither should be an <a>
    expect(outerDiv.tagName).toBe('DIV');
    const cardEl = outerDiv.firstChild as HTMLElement;
    expect(cardEl.tagName).toBe('DIV');
  });

  it('does not contain a nested <a> inside another <a> (no validateDOMNesting warning)', () => {
    const { container } = render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
        })}
      />,
    );

    const anchors = container.querySelectorAll('a');
    // Every anchor must NOT be a descendant of another anchor.
    anchors.forEach((anchor) => {
      let parent = anchor.parentElement;
      while (parent) {
        expect(parent.tagName).not.toBe('A');
        parent = parent.parentElement;
      }
    });
  });

  it('renders an explicit "View ROI" link when gains > 0', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
        })}
      />,
    );

    const roiLink = screen.getByRole('link', { name: /view full roi dashboard/i });
    expect(roiLink).toBeInTheDocument();
  });

  it('renders the inline $/click input and Save button when hasLift && !showRevenue', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          revenue_estimate: null,
          roi_per_click_value: null,
        })}
      />,
    );

    expect(screen.getByTestId('recovery-hero-cpc-nudge')).toBeInTheDocument();
    expect(screen.getByTestId('recovery-hero-cpc-input')).toBeInTheDocument();
    expect(screen.getByTestId('recovery-hero-cpc-save')).toBeInTheDocument();
    // Save button should be labelled
    expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
  });

  it('pre-fills the input with the stored roi_per_click_value when provided', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          revenue_estimate: null,
          roi_per_click_value: 0.75,
        })}
      />,
    );

    const input = screen.getByTestId('recovery-hero-cpc-input') as HTMLInputElement;
    expect(input.value).toBe('0.75');
  });

  it('Save button posts to settings.roi-per-click.store via router.post (fire-and-forget)', async () => {
    const { fireEvent } = await import('@testing-library/react');
    const { router } = await import('@inertiajs/react');
    const postSpy = vi.mocked(router.post);

    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          revenue_estimate: null,
          roi_per_click_value: null,
        })}
      />,
    );

    const input = screen.getByTestId('recovery-hero-cpc-input');
    fireEvent.change(input, { target: { value: '0.50' } });
    fireEvent.click(screen.getByTestId('recovery-hero-cpc-save'));

    expect(postSpy).toHaveBeenCalledWith(
      '/settings/roi-per-click', // route mock resolves to this
      { per_click_value: 0.5 },
      expect.objectContaining({ onFinish: expect.any(Function) }),
    );
  });

  it('Save button shows disabled state while saving', async () => {
    const { fireEvent } = await import('@testing-library/react');

    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          revenue_estimate: null,
          roi_per_click_value: null,
        })}
      />,
    );

    const input = screen.getByTestId('recovery-hero-cpc-input');
    fireEvent.change(input, { target: { value: '0.50' } });
    fireEvent.click(screen.getByTestId('recovery-hero-cpc-save'));

    // Button should be disabled immediately after click (before onFinish fires)
    const saveBtn = screen.getByTestId('recovery-hero-cpc-save');
    expect(saveBtn).toBeDisabled();
  });

  it('does NOT render the inline rate form when revenue_estimate is already set (showRevenue=true)', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 120,
          revenue_estimate: 60,
          revenue_estimate_label: 'at your $0.50/click — your estimate',
          roi_per_click_value: 0.50,
        })}
      />,
    );

    expect(screen.queryByTestId('recovery-hero-cpc-input')).not.toBeInTheDocument();
    expect(screen.queryByTestId('recovery-hero-cpc-save')).not.toBeInTheDocument();
  });

  it('does NOT render the inline rate form when gains are 0', () => {
    render(
      <RecoveryHero
        recoveredTraffic={makeTraffic({
          measured_clicks_gained: 0,
          revenue_estimate: null,
          roi_per_click_value: null,
        })}
      />,
    );

    expect(screen.queryByTestId('recovery-hero-cpc-input')).not.toBeInTheDocument();
  });
});
