import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { toast } from 'sonner';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

// R3UXA-001: mock copyToClipboard to test clipboard failure/success
const { mockCopyToClipboard } = vi.hoisted(() => ({ mockCopyToClipboard: vi.fn() }));
vi.mock('@/lib/clipboard', () => ({ copyToClipboard: mockCopyToClipboard }));

vi.mock('sonner', () => ({
  toast: { success: vi.fn(), error: vi.fn() },
}));

import { useForm, usePage } from '@inertiajs/react';

import Security from './Security';

const mockPost = vi.fn();
const mockDelete = vi.fn();
const mockSetData = vi.fn();
let mockIsDirty = false;

vi.mock('@inertiajs/react', async () => {
  const actual = await vi.importActual('@inertiajs/react');
  return {
    ...actual,
    useForm: vi.fn(() => ({
      data: {},
      setData: mockSetData,
      post: mockPost,
      delete: mockDelete,
      processing: false,
      errors: {},
      get isDirty() {
        return mockIsDirty;
      },
    })),
    usePage: vi.fn(() => ({
      props: {
        auth: { user: { id: 1, name: 'Test', email: 'test@example.com', has_password: true } },
        flash: {},
        errors: {},
        features: {
          twoFactor: true,
          billing: false,
          socialAuth: false,
          emailVerification: true,
          apiTokens: true,
          userSettings: true,
          notifications: false,
          onboarding: false,
          apiDocs: false,
        },
        notifications_unread_count: 0,
      },
    })),
    Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
      <a href={href}>{children}</a>
    ),
    Head: ({ title }: { title: string }) => <title>{title}</title>,
    router: { post: vi.fn(), on: vi.fn(() => vi.fn()) },
  };
});

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

vi.mock('@/Components/layout/PageHeader', () => ({
  default: ({ title, subtitle }: { title: string; subtitle?: string }) => (
    <div data-testid="page-header">
      <h1>{title}</h1>
      {subtitle && <p>{subtitle}</p>}
    </div>
  ),
}));

vi.mock('@/Components/ui/input-otp', () => ({
  InputOTP: ({
    children,
    onChange,
    ...props
  }: {
    children: React.ReactNode;
    onChange?: (val: string) => void;
    [key: string]: unknown;
  }) => (
    <div data-testid="input-otp" {...props}>
      <input
        data-testid="otp-input"
        onChange={(e) => onChange?.(e.target.value)}
        aria-label="Verification code"
      />
      {children}
    </div>
  ),
  InputOTPGroup: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  InputOTPSlot: ({ index }: { index: number }) => <div data-testid={`otp-slot-${index}`} />,
}));

vi.mock('@/Components/ui/confirm-dialog', () => ({
  ConfirmDialog: ({
    open,
    title,
    description,
  }: {
    open: boolean;
    title: string;
    description: React.ReactNode;
  }) =>
    open ? (
      <div data-testid="confirm-dialog">
        <h2>{title}</h2>
        <div>{description}</div>
      </div>
    ) : null,
}));

const mockedUseForm = vi.mocked(useForm);
const mockedUsePage = vi.mocked(usePage);

describe('Security Page', () => {
  const user = userEvent.setup();
  let addSpy: ReturnType<typeof vi.spyOn>;
  let removeSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockIsDirty = false;
    // Default: clipboard succeeds
    mockCopyToClipboard.mockResolvedValue(true);
    mockedUseForm.mockReturnValue({
      data: {},
      setData: mockSetData,
      post: mockPost,
      delete: mockDelete,
      processing: false,
      errors: {},
      get isDirty() {
        return mockIsDirty;
      },
    } as unknown as ReturnType<typeof useForm>);
    mockedUsePage.mockReturnValue({
      props: {
        auth: { user: { id: 1, name: 'Test', email: 'test@example.com', has_password: true } },
        flash: {},
        errors: {},
        features: {
          twoFactor: true,
          billing: false,
          socialAuth: false,
          emailVerification: true,
          apiTokens: true,
          userSettings: true,
          notifications: false,
          onboarding: false,
          apiDocs: false,
        },
        notifications_unread_count: 0,
      },
    } as unknown as ReturnType<typeof usePage>);
    addSpy = vi.spyOn(window, 'addEventListener');
    removeSpy = vi.spyOn(window, 'removeEventListener');
  });

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

  describe('not-enabled state', () => {
    it('renders the security page with DashboardLayout', () => {
      render(<Security enabled={false} qr_code={null} secret={null} recovery_codes={null} />);

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

    it('renders page header', () => {
      render(<Security enabled={false} qr_code={null} secret={null} recovery_codes={null} />);

      expect(screen.getByTestId('page-header')).toBeInTheDocument();
      expect(screen.getByRole('heading', { name: /security/i })).toBeInTheDocument();
    });

    it('shows not enabled badge', () => {
      render(<Security enabled={false} qr_code={null} secret={null} recovery_codes={null} />);

      expect(screen.getByText(/not enabled/i)).toBeInTheDocument();
    });

    it('shows enable button', () => {
      render(<Security enabled={false} qr_code={null} secret={null} recovery_codes={null} />);

      expect(
        screen.getByRole('button', { name: /enable two-factor authentication/i }),
      ).toBeInTheDocument();
    });

    it('submits enable form', async () => {
      render(<Security enabled={false} qr_code={null} secret={null} recovery_codes={null} />);

      const enableButton = screen.getByRole('button', {
        name: /enable two-factor authentication/i,
      });
      await user.click(enableButton);

      expect(mockPost).toHaveBeenCalled();
    });
  });

  describe('setup state', () => {
    it('shows QR code when in setup', () => {
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      expect(screen.getByText(/scan the qr code/i)).toBeInTheDocument();
    });

    it('shows manual entry key', () => {
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

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

    it('shows verification code input', () => {
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      expect(screen.getByTestId('input-otp')).toBeInTheDocument();
    });

    it('shows confirm button', () => {
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      expect(screen.getByRole('button', { name: /confirm and enable/i })).toBeInTheDocument();
    });

    it('shows copy secret button', () => {
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      expect(screen.getByRole('button', { name: /copy secret key/i })).toBeInTheDocument();
    });
  });

  describe('enabled state', () => {
    it('shows enabled badge', () => {
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

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

    it('shows protection message', () => {
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

      expect(screen.getByText(/account is protected/i)).toBeInTheDocument();
    });

    it('shows view recovery codes button', () => {
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

      expect(screen.getByRole('button', { name: /view recovery codes/i })).toBeInTheDocument();
    });

    it('shows regenerate codes button', () => {
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

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

    it('shows disable button', () => {
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

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

    it('opens confirm dialog when disable clicked', async () => {
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

      await user.click(screen.getByRole('button', { name: /disable/i }));

      expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument();
    });
  });

  describe('flash messages', () => {
    it('shows success flash message', () => {
      mockedUsePage.mockReturnValue({
        props: {
          auth: { user: { id: 1, name: 'Test', email: 'test@example.com', has_password: true } },
          flash: { success: 'Two-factor authentication has been enabled.' },
          errors: {},
          features: {
            twoFactor: true,
            billing: false,
            socialAuth: false,
            emailVerification: true,
            apiTokens: true,
            userSettings: true,
            notifications: false,
            onboarding: false,
            apiDocs: false,
          },
          notifications_unread_count: 0,
        },
      } as unknown as ReturnType<typeof usePage>);

      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

      expect(screen.getByText(/two-factor authentication has been enabled/i)).toBeInTheDocument();
    });
  });

  describe('unsaved changes warning - setup state', () => {
    it('registers beforeunload listener in setup state', () => {
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

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

    it('removes listener on unmount in setup state', () => {
      const { unmount } = render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      unmount();

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

    it('prevents unload when form is dirty in setup state', () => {
      mockIsDirty = true;
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      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 in setup state', () => {
      mockIsDirty = false;
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      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();
    });
  });

  describe('unsaved changes warning - enabled state', () => {
    it('registers beforeunload listener in enabled state', () => {
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

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

    it('removes listener on unmount in enabled state', () => {
      const { unmount } = render(
        <Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />,
      );

      unmount();

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

    it('prevents unload when form is dirty in enabled state', () => {
      mockIsDirty = true;
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

      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 in enabled state', () => {
      mockIsDirty = false;
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

      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();
    });
  });

  // ============================================
  // R3UXA-001: hardened copy — clipboard failure shows error toast (setup state copy secret)
  // ============================================

  describe('R3UXA-001: hardened clipboard copy in setup state', () => {
    it('shows error toast when copying the 2FA secret fails', async () => {
      mockCopyToClipboard.mockResolvedValue(false);
      const user = userEvent.setup();
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      vi.mocked(toast.success).mockClear();
      vi.mocked(toast.error).mockClear();

      await user.click(screen.getByRole('button', { name: /copy secret key/i }));

      expect(vi.mocked(toast.error)).toHaveBeenCalledTimes(1);
      expect(vi.mocked(toast.success)).not.toHaveBeenCalled();
    });

    it('shows success toast when copying the 2FA secret succeeds', async () => {
      mockCopyToClipboard.mockResolvedValue(true);
      const user = userEvent.setup();
      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      vi.mocked(toast.success).mockClear();
      vi.mocked(toast.error).mockClear();

      await user.click(screen.getByRole('button', { name: /copy secret key/i }));

      expect(vi.mocked(toast.success)).toHaveBeenCalledTimes(1);
      expect(vi.mocked(toast.error)).not.toHaveBeenCalled();
    });
  });

  // ============================================
  // R3UXA-015: force-present recovery codes at enablement — require acknowledgment
  // ============================================

  describe('R3UXA-015: one-time recovery codes at 2FA enablement', () => {
    it('force-shows recovery codes when recovery_codes prop is populated (just enabled)', () => {
      render(
        <Security
          enabled={true}
          qr_code={null}
          secret={null}
          recovery_codes={['code-1111', 'code-2222']}
        />,
      );

      // Recovery codes panel should be visible immediately
      expect(screen.getByText('code-1111')).toBeInTheDocument();
      expect(screen.getByText('code-2222')).toBeInTheDocument();
    });

    it('shows the acknowledgment checkbox when recovery_codes prop is populated', () => {
      render(
        <Security
          enabled={true}
          qr_code={null}
          secret={null}
          recovery_codes={['code-1111', 'code-2222']}
        />,
      );

      expect(
        screen.getByRole('checkbox', { name: /I.ve stored my recovery codes/i }),
      ).toBeInTheDocument();
    });

    it('Hide recovery codes button is disabled until acknowledgment is ticked', () => {
      render(
        <Security
          enabled={true}
          qr_code={null}
          secret={null}
          recovery_codes={['code-1111', 'code-2222']}
        />,
      );

      // The "Hide recovery codes" button (shown when panel is open) should be disabled
      const hideBtn = screen.getByRole('button', { name: /hide recovery codes/i });
      expect(hideBtn).toBeDisabled();
    });

    it('acknowledgment checkbox enables the Hide button and allows dismissal', async () => {
      const user = userEvent.setup();
      render(
        <Security
          enabled={true}
          qr_code={null}
          secret={null}
          recovery_codes={['code-1111', 'code-2222']}
        />,
      );

      const checkbox = screen.getByRole('checkbox', { name: /I.ve stored my recovery codes/i });
      const hideBtn = screen.getByRole('button', { name: /hide recovery codes/i });

      expect(hideBtn).toBeDisabled();
      await user.click(checkbox);
      expect(hideBtn).not.toBeDisabled();

      await user.click(hideBtn);

      // After dismissal the codes should no longer be visible
      await waitFor(() => {
        expect(screen.queryByText('code-1111')).not.toBeInTheDocument();
      });
    });

    it('does NOT show acknowledgment checkbox when no recovery_codes prop (normal view)', () => {
      render(<Security enabled={true} qr_code={null} secret={null} recovery_codes={null} />);

      expect(
        screen.queryByRole('checkbox', { name: /I.ve stored my recovery codes/i }),
      ).not.toBeInTheDocument();
    });
  });

  // ============================================
  // F5A11Y-002: aria-invalid + aria-describedby linkage for server validation errors
  // ============================================

  describe('F5A11Y-002: server error ARIA linkage in setup state', () => {
    it('OTP input gets aria-invalid=true when a code error is present', () => {
      mockedUseForm.mockReturnValue({
        data: { code: '' },
        setData: mockSetData,
        post: mockPost,
        delete: mockDelete,
        processing: false,
        errors: { code: 'The verification code is incorrect.' },
        get isDirty() { return false; },
      } as unknown as ReturnType<typeof useForm>);

      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      // The InputOTP mock renders a div with id="confirm-code"; the inner
      // input-otp div should forward aria-invalid when the prop is set.
      const otpRoot = screen.getByTestId('input-otp');
      expect(otpRoot).toHaveAttribute('aria-invalid', 'true');
    });

    it('OTP input is linked to the error message via aria-describedby', () => {
      mockedUseForm.mockReturnValue({
        data: { code: '' },
        setData: mockSetData,
        post: mockPost,
        delete: mockDelete,
        processing: false,
        errors: { code: 'The verification code is incorrect.' },
        get isDirty() { return false; },
      } as unknown as ReturnType<typeof useForm>);

      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      const otpRoot = screen.getByTestId('input-otp');
      expect(otpRoot).toHaveAttribute('aria-describedby', 'confirm-code-error');
    });

    it('error message rendered by InputError carries role=alert', () => {
      mockedUseForm.mockReturnValue({
        data: { code: '' },
        setData: mockSetData,
        post: mockPost,
        delete: mockDelete,
        processing: false,
        errors: { code: 'The verification code is incorrect.' },
        get isDirty() { return false; },
      } as unknown as ReturnType<typeof useForm>);

      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      // InputError renders a <p role="alert"> when message is present.
      expect(screen.getByRole('alert')).toHaveTextContent(
        'The verification code is incorrect.',
      );
    });

    it('error <p> has the id that the OTP input references via aria-describedby', () => {
      mockedUseForm.mockReturnValue({
        data: { code: '' },
        setData: mockSetData,
        post: mockPost,
        delete: mockDelete,
        processing: false,
        errors: { code: 'The verification code is incorrect.' },
        get isDirty() { return false; },
      } as unknown as ReturnType<typeof useForm>);

      render(
        <Security
          enabled={false}
          qr_code="<svg>mock-qr</svg>"
          secret="ABCDEF123456"
          recovery_codes={null}
        />,
      );

      const alert = screen.getByRole('alert');
      expect(alert).toHaveAttribute('id', 'confirm-code-error');
    });
  });
});
