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

vi.mock('@inertiajs/react', async () => {
  const { createElement } = await import('react');
  return {
    Link: ({
      href,
      children,
      onClick,
      className,
    }: {
      href: string;
      children: unknown;
      onClick?: () => void;
      className?: string;
    }) =>
      createElement(
        'a',
        { href, onClick, className },
        children as Parameters<typeof createElement>[2],
      ),
  };
});

vi.mock('@/lib/analytics', () => ({ trackEvent: vi.fn() }));
vi.mock('@/lib/event-catalog', () => ({ BLOG_TO_PRODUCT_CTA_CLICK: 'blog_to_product_cta_click' }));

import { BlogProductCta } from '@/Components/marketing/BlogProductCta';
import {
  getBlogVisitCount,
  incrementBlogVisitCount,
} from '@/lib/blog-visit-counter';

const SLUG = 'test-post';
const STORAGE_KEY = `rwz_blog_visit_${SLUG}`;
const SESSION_KEY = `rwz_blog_session_${SLUG}`;
const TTL_MS = 30 * 24 * 60 * 60 * 1000;

beforeEach(() => {
  localStorage.clear();
  sessionStorage.clear();
  vi.clearAllMocks();
});

// ---------------------------------------------------------------------------
// getBlogVisitCount unit tests
// ---------------------------------------------------------------------------
describe('getBlogVisitCount', () => {
  it('returns 0 when no stored value', () => {
    expect(getBlogVisitCount(SLUG)).toBe(0);
  });

  it('returns stored count within TTL', () => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ count: 2, ts: Date.now() }));
    expect(getBlogVisitCount(SLUG)).toBe(2);
  });

  it('returns 0 when TTL has expired', () => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ count: 5, ts: Date.now() - TTL_MS - 1000 }));
    expect(getBlogVisitCount(SLUG)).toBe(0);
  });

  it('returns 0 for corrupted non-numeric count (Number.isFinite guard)', () => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ count: 'corrupted', ts: Date.now() }));
    expect(getBlogVisitCount(SLUG)).toBe(0);
  });

  it('returns 0 for non-JSON garbage value (parse guard)', () => {
    localStorage.setItem(STORAGE_KEY, 'not-json');
    expect(getBlogVisitCount(SLUG)).toBe(0);
  });

  it('uses separate key per slug (no cross-slug contamination)', () => {
    localStorage.setItem('rwz_blog_visit_slug-a', JSON.stringify({ count: 3, ts: Date.now() }));
    expect(getBlogVisitCount('slug-b')).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// incrementBlogVisitCount unit tests
// ---------------------------------------------------------------------------
describe('incrementBlogVisitCount', () => {
  it('increments count from 0 to 1', () => {
    incrementBlogVisitCount(SLUG);
    expect(getBlogVisitCount(SLUG)).toBe(1);
  });

  it('sets the session guard after a successful increment', () => {
    incrementBlogVisitCount(SLUG);
    expect(sessionStorage.getItem(SESSION_KEY)).toBe('1');
  });

  it('does not double-increment on remount (session guard)', () => {
    incrementBlogVisitCount(SLUG);
    incrementBlogVisitCount(SLUG);
    expect(getBlogVisitCount(SLUG)).toBe(1);
  });

  it('does not set session guard when localStorage write fails', () => {
    // Replace window.localStorage with a broken implementation whose setItem always throws.
    // vi.spyOn on the instance is unreliable in jsdom because setItem lives on Storage.prototype;
    // Object.defineProperty on window gives us a fully controlled replacement.
    const originalDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage');
    const brokenLocalStorage = {
      getItem: () => null,
      setItem: () => {
        throw new Error('QuotaExceededError');
      },
      removeItem: () => undefined,
      clear: () => undefined,
      key: () => null,
      length: 0,
    };
    Object.defineProperty(window, 'localStorage', {
      value: brokenLocalStorage,
      configurable: true,
    });

    try {
      incrementBlogVisitCount(SLUG);
      expect(sessionStorage.getItem(SESSION_KEY)).toBeNull();
    } finally {
      if (originalDescriptor) {
        Object.defineProperty(window, 'localStorage', originalDescriptor);
      }
    }
  });

  it('does not throw when localStorage is unavailable', () => {
    const originalDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage');
    const brokenLocalStorage = {
      getItem: () => {
        throw new Error('SecurityError');
      },
      setItem: () => {
        throw new Error('SecurityError');
      },
      removeItem: () => undefined,
      clear: () => undefined,
      key: () => null,
      length: 0,
    };
    Object.defineProperty(window, 'localStorage', {
      value: brokenLocalStorage,
      configurable: true,
    });
    try {
      expect(() => incrementBlogVisitCount(SLUG)).not.toThrow();
    } finally {
      if (originalDescriptor) {
        Object.defineProperty(window, 'localStorage', originalDescriptor);
      }
    }
  });

  it('does not throw when sessionStorage is unavailable', () => {
    vi.spyOn(sessionStorage, 'getItem').mockImplementation(() => {
      throw new Error('SecurityError');
    });
    vi.spyOn(sessionStorage, 'setItem').mockImplementation(() => {
      throw new Error('SecurityError');
    });
    // Should still increment localStorage since the guard check failure is caught
    expect(() => incrementBlogVisitCount(SLUG)).not.toThrow();
  });

  it('increments per-slug independently', () => {
    incrementBlogVisitCount('slug-a');
    incrementBlogVisitCount('slug-a');
    incrementBlogVisitCount('slug-b');
    // Each slug has its own session guard; slug-a blocked at 1, slug-b at 1
    expect(getBlogVisitCount('slug-a')).toBe(1);
    expect(getBlogVisitCount('slug-b')).toBe(1);
  });
});

// ---------------------------------------------------------------------------
// BlogProductCta component tests — label personalization branching
// ---------------------------------------------------------------------------
describe('BlogProductCta — label personalization', () => {
  it('renders default CTA label below threshold (bottom variant)', async () => {
    // No stored count — first visit
    await act(async () => {
      render(<BlogProductCta category={null} blogSlug={SLUG} variant="bottom" />);
    });
    expect(screen.getByRole('link', { name: /try free — no credit card/i })).toBeInTheDocument();
  });

  it('renders returning-user label at RETURNING_THRESHOLD (bottom variant)', async () => {
    // Pre-seed count at threshold - 1 so after increment it hits threshold
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ count: 2, ts: Date.now() }));

    await act(async () => {
      render(<BlogProductCta category={null} blogSlug={SLUG} variant="bottom" />);
    });

    expect(screen.getByRole('link', { name: /start pro trial/i })).toBeInTheDocument();
  });

  it('renders returning-user label above threshold', async () => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ count: 5, ts: Date.now() }));
    // Pre-seed session guard so increment is skipped (count stays 5)
    sessionStorage.setItem(SESSION_KEY, '1');

    await act(async () => {
      render(<BlogProductCta category={null} blogSlug={SLUG} variant="bottom" />);
    });

    expect(screen.getByRole('link', { name: /start pro trial/i })).toBeInTheDocument();
  });

  it('renders default CTA label when TTL has expired', async () => {
    // Expired count of 5 — after increment from 0 the count becomes 1 (below threshold)
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ count: 5, ts: Date.now() - TTL_MS - 1000 }));

    await act(async () => {
      render(<BlogProductCta category={null} blogSlug={SLUG} variant="bottom" />);
    });

    expect(screen.getByRole('link', { name: /try free — no credit card/i })).toBeInTheDocument();
  });

  it('renders default CTA label below threshold (inline variant)', async () => {
    await act(async () => {
      render(<BlogProductCta category={null} blogSlug={SLUG} variant="inline" />);
    });
    expect(screen.getByRole('link', { name: /try free — no credit card/i })).toBeInTheDocument();
  });

  it('renders returning-user label at threshold (inline variant)', async () => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ count: 2, ts: Date.now() }));

    await act(async () => {
      render(<BlogProductCta category={null} blogSlug={SLUG} variant="inline" />);
    });

    expect(screen.getByRole('link', { name: /start pro trial/i })).toBeInTheDocument();
  });

  it('does not crash when localStorage is unavailable', async () => {
    const originalDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage');
    const brokenLocalStorage = {
      getItem: () => {
        throw new Error('SecurityError');
      },
      setItem: () => {
        throw new Error('QuotaExceededError');
      },
      removeItem: () => undefined,
      clear: () => undefined,
      key: () => null,
      length: 0,
    };
    Object.defineProperty(window, 'localStorage', {
      value: brokenLocalStorage,
      configurable: true,
    });
    try {
      await expect(
        act(async () => {
          render(<BlogProductCta category={null} blogSlug={SLUG} />);
        }),
      ).resolves.not.toThrow();
    } finally {
      if (originalDescriptor) {
        Object.defineProperty(window, 'localStorage', originalDescriptor);
      }
    }
  });

  it('re-increments and re-reads when blogSlug prop changes', async () => {
    const { rerender } = render(<BlogProductCta category={null} blogSlug="slug-a" />);

    await act(async () => {
      rerender(<BlogProductCta category={null} blogSlug="slug-b" />);
    });

    // slug-b should now have a count of 1
    expect(getBlogVisitCount('slug-b')).toBe(1);
    // slug-a had a count of 1 from initial render
    expect(getBlogVisitCount('slug-a')).toBe(1);
  });
});
