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

import { getRouteCanonicalLabel } from '@/config/site-navigation';

const { mockTrackProductEvent } = vi.hoisted(() => ({ mockTrackProductEvent: vi.fn() }));
vi.mock('@/lib/analytics', () => ({ trackProductEvent: mockTrackProductEvent }));

const mockPatch = vi.fn();
let mockIsDirty = false;
let mockErrors: Record<string, string> = {};
let mockFlash: Record<string, string> = {};

vi.mock('@inertiajs/react', async () => {
  const actual = await vi.importActual('@inertiajs/react');
  return {
    ...actual,
    Head: ({ title }: { title: string }) => <title>{title}</title>,
    useForm: (initialData: Record<string, unknown>) => ({
      data: initialData,
      setData: vi.fn(),
      patch: mockPatch,
      processing: false,
      get errors() {
        return mockErrors;
      },
      get isDirty() {
        return mockIsDirty;
      },
    }),
    usePage: () => ({
      props: {
        get flash() {
          return mockFlash;
        },
        // Required by Notifications: sites for otherSiteCount + features for
        // lifecycle_plg visibility guard (F4UXA-005). Without these the component
        // throws TypeError on features.teams and sites?.length.
        sites: [],
        features: { teams: false, billing: false },
      },
    }),
  };
});

const { mockFireToast } = vi.hoisted(() => ({ mockFireToast: vi.fn() }));
vi.mock('@/lib/toast-config', () => ({ fireToast: mockFireToast }));

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

import Notifications from './Notifications';

const baseSite = {
  id: 1,
  name: 'Test Site',
};

const noSettingsProps = {
  site: baseSite,
  settings: null,
  flash: {},
};

const withSettingsProps = {
  site: baseSite,
  settings: {
    id: 1,
    site_id: 1,
    digest_frequency: 'daily' as const,
    notification_types: ['analysis_complete', 'traffic_alert'],
    digest_type_frequencies: null,
    last_digest_sent_at: '2026-02-20T10:00:00Z',
  },
  flash: {},
};

describe('Notifications Settings Page', () => {
  let addSpy: ReturnType<typeof vi.spyOn>;
  let removeSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockIsDirty = false;
    mockErrors = {};
    mockFlash = {};
    vi.stubGlobal('route', (name: string, params?: Record<string, unknown>) => {
      if (params) {
        return `/mocked/${name}/${params.site}`;
      }
      return `/mocked/${name}`;
    });
    addSpy = vi.spyOn(window, 'addEventListener');
    removeSpy = vi.spyOn(window, 'removeEventListener');
  });

  afterEach(() => {
    addSpy.mockRestore();
    removeSpy.mockRestore();
  });

  it('fires settings_notifications_viewed and feature_used on mount', () => {
    render(<Notifications {...noSettingsProps} />);
    expect(mockTrackProductEvent).toHaveBeenCalledWith('settings_notifications_viewed');
    expect(mockTrackProductEvent).toHaveBeenCalledWith('feature_used', {
      feature: 'settings_notifications',
    });
  });

  it('renders page title with site name', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(document.title).toBe(`${getRouteCanonicalLabel('notification-settings.show')} - Test Site`);
  });

  it('renders heading "Email Notification Settings"', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(
      screen.getByRole('heading', { name: /email notification settings/i }),
    ).toBeInTheDocument();
  });

  it('renders site name in description', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(
      screen.getByText(/Choose what gets emailed and when/i),
    ).toBeInTheDocument();
  });

  it('shows "Enabled" badge by default (new settings default to weekly — W2-26)', () => {
    // settings: null → form defaults to 'weekly' (not 'off'), so badge is Enabled
    render(<Notifications {...noSettingsProps} />);

    expect(screen.getByText('Enabled')).toBeInTheDocument();
  });

  it('shows "Disabled" badge when digest is explicitly off', () => {
    const offProps = {
      ...noSettingsProps,
      settings: {
        id: 1,
        site_id: 1,
        digest_frequency: 'off' as const,
        notification_types: [],
        digest_type_frequencies: null,
        last_digest_sent_at: null,
      },
    };
    render(<Notifications {...offProps} />);

    expect(screen.getByText('Disabled')).toBeInTheDocument();
  });

  it('shows "Enabled" badge when digest is not off', () => {
    render(<Notifications {...withSettingsProps} />);

    expect(screen.getByText('Enabled')).toBeInTheDocument();
  });

  it('renders digest frequency dropdown', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(screen.getByLabelText(/digest frequency/i)).toBeInTheDocument();
  });

  it('renders all four notification type checkboxes', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(screen.getByText('Analysis Complete')).toBeInTheDocument();
    expect(screen.getByText('New Recommendations')).toBeInTheDocument();
    expect(screen.getByText('Traffic Alerts')).toBeInTheDocument();
    expect(screen.getByText('Connection Failures')).toBeInTheDocument();
  });

  it('renders notification type descriptions', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(screen.getByText(/include analysis completion summaries/i)).toBeInTheDocument();
    expect(screen.getByText(/include new seo recommendation highlights/i)).toBeInTheDocument();
    expect(screen.getByText(/include traffic change alerts/i)).toBeInTheDocument();
    expect(screen.getByText(/include connection failure notices/i)).toBeInTheDocument();
  });

  it('renders critical alerts info box', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(screen.getByText(/critical alerts.*transactional emails/i)).toBeInTheDocument();
    expect(screen.getByText(/some emails are sent immediately regardless/i)).toBeInTheDocument();
  });

  it('renders save button', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(screen.getByRole('button', { name: /save settings/i })).toBeInTheDocument();
  });

  it('shows last digest sent date when available', () => {
    render(<Notifications {...withSettingsProps} />);

    expect(screen.getByText(/last digest sent:/i)).toBeInTheDocument();
  });

  it('does not show last digest sent when no settings', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(screen.queryByText(/last digest sent:/i)).not.toBeInTheDocument();
  });

  it('renders within dashboard layout', () => {
    render(<Notifications {...noSettingsProps} />);

    expect(screen.getByTestId('dashboard-layout')).toBeInTheDocument();
  });

  describe('persona v1 — collapsed advanced lifecycle preferences', () => {
    it('collapses per-type frequency + lifecycle categories behind an "Advanced preferences" disclosure', () => {
      render(<Notifications {...noSettingsProps} />);

      // R6UXA-006: renamed from "Advanced email preferences" to "Advanced preferences"
      expect(
        screen.getByText(/advanced preferences/i),
      ).toBeInTheDocument();
    });

    it('hides lifecycle categories (e.g. Win-Back Emails) by default', () => {
      render(<Notifications {...noSettingsProps} />);

      // The four core digests are always visible…
      expect(screen.getByText('Analysis Complete')).toBeInTheDocument();
      // …but the CRM/lifecycle toggles are not rendered until the disclosure opens.
      expect(screen.queryByText('Win-Back Emails')).not.toBeInTheDocument();
      expect(screen.queryByText('Team & Collaboration Nudges')).not.toBeInTheDocument();
    });

    it('does not show inline per-type frequency dropdowns in the core section (R6UXA-006)', () => {
      // The core four checkboxes must NOT have an inline <Select> (combobox) next to them.
      // Frequency selectors are in the Advanced section only.
      render(<Notifications {...noSettingsProps} />);

      // With advanced collapsed, there should be no comboboxes visible
      // (the global digest-frequency combobox has id="digest-frequency"; any additional
      // combobox would be a per-type frequency select that leaked into the core section).
      const comboboxes = screen.queryAllByRole('combobox');
      // Only the one global digest-frequency dropdown should be present
      expect(comboboxes).toHaveLength(1);
      expect(comboboxes[0]).toHaveAttribute('id', 'digest-frequency');
    });

    it('reveals lifecycle categories when the disclosure is expanded', async () => {
      const { default: userEvent } = await import('@testing-library/user-event');
      const user = userEvent.setup();
      render(<Notifications {...noSettingsProps} />);

      await user.click(screen.getByText(/advanced preferences/i));

      expect(screen.getByText('Win-Back Emails')).toBeInTheDocument();
    });
  });

  describe('flash + error feedback', () => {
    it('fires a toast for a flash error so failed saves show feedback', () => {
      mockFlash = { error: 'Could not save your notification settings.' };
      render(<Notifications {...noSettingsProps} />);

      expect(mockFireToast).toHaveBeenCalledWith(
        'error',
        'Could not save your notification settings.',
      );
    });

    it('renders inline validation errors from Inertia errors', () => {
      mockErrors = { digest_frequency: 'The selected digest frequency is invalid.' };
      render(<Notifications {...noSettingsProps} />);

      expect(
        screen.getByText(/the selected digest frequency is invalid/i),
      ).toBeInTheDocument();
    });
  });

  describe('unsaved changes warning', () => {
    it('registers beforeunload listener', () => {
      render(<Notifications {...noSettingsProps} />);

      expect(addSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
    });

    it('removes listener on unmount', () => {
      const { unmount } = render(<Notifications {...noSettingsProps} />);

      unmount();

      expect(removeSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
    });

    it('prevents unload when form is dirty', () => {
      mockIsDirty = true;
      render(<Notifications {...noSettingsProps} />);

      const handler = addSpy.mock.calls.find(
        ([event]: [string, ...unknown[]]) => event === 'beforeunload',
      )![1] as EventListener;

      const event = new Event('beforeunload') as BeforeUnloadEvent;
      const preventSpy = vi.spyOn(event, 'preventDefault');

      handler(event);

      expect(preventSpy).toHaveBeenCalled();
    });

    it('does not prevent unload when form is not dirty', () => {
      mockIsDirty = false;
      render(<Notifications {...noSettingsProps} />);

      const handler = addSpy.mock.calls.find(
        ([event]: [string, ...unknown[]]) => event === 'beforeunload',
      )![1] as EventListener;

      const event = new Event('beforeunload') as BeforeUnloadEvent;
      const preventSpy = vi.spyOn(event, 'preventDefault');

      handler(event);

      expect(preventSpy).not.toHaveBeenCalled();
    });
  });
});
