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

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

const mockPost = vi.fn();
const mockSetData = vi.fn();
const mockReset = vi.fn();

vi.mock('@inertiajs/react', async () => {
  const actual = await vi.importActual('@inertiajs/react');
  return {
    ...actual,
    usePage: vi.fn(() => ({
      props: {
        auth: { user: { name: 'Test User', email: 'test@example.com' } },
        features: { billing: false, notifications: false },
        sites: [],
        limits: null,
        ai_defaults: { model: 'gpt-4o-mini', temperature: 0.7 },
      },
    })),
    useForm: vi.fn(() => ({
      data: { provider: 'dataforseo', login: '', password: '' },
      setData: mockSetData,
      post: mockPost,
      reset: mockReset,
      processing: false,
      errors: {},
      isDirty: false,
    })),
    Head: () => null,
    Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
      <a href={href}>{children}</a>
    ),
  };
});

vi.mock('@/Components/theme/use-theme', () => ({
  useTheme: vi.fn(() => ({ theme: 'system', setTheme: vi.fn(), resolvedTheme: 'light' })),
}));

vi.mock('@/hooks/useFlashToasts', () => ({ useFlashToasts: vi.fn() }));
vi.mock('@/hooks/useUnsavedChanges', () => ({ useUnsavedChanges: vi.fn() }));

import SerpKeySettings from './SerpKey';
import { validationInfoMap as serpValidationInfoMap } from './serpValidationInfo';
import { validationInfoMap as aiValidationInfoMap } from './validationInfo';

const defaultProps = {
  has_key: false,
  masked_login: null,
  provider: null,
  is_validated: false,
  validated_at: null,
  key_validation: null,
};

describe('SerpKeySettings', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('renders the SERP API Settings heading', () => {
    render(<SerpKeySettings {...defaultProps} />);
    expect(screen.getByText(/serp api settings/i)).toBeInTheDocument();
  });

  it('renders login and password inputs when no key is configured', () => {
    render(<SerpKeySettings {...defaultProps} />);
    expect(screen.getByLabelText(/login/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i, { selector: 'input' })).toBeInTheDocument();
  });

  it('calls setData when typing login', async () => {
    const user = userEvent.setup();
    render(<SerpKeySettings {...defaultProps} />);

    await user.type(screen.getByLabelText(/login/i), 'user@example.com');

    expect(mockSetData).toHaveBeenCalledWith('login', expect.any(String));
  });

  it('calls post to store route on form submit', async () => {
    const user = userEvent.setup();
    render(<SerpKeySettings {...defaultProps} />);

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

    // R3FE-012: onSuccess callback now included to reset secret fields after save
    expect(mockPost).toHaveBeenCalledWith('/settings/serp', expect.objectContaining({ onSuccess: expect.any(Function) }));
  });

  it('shows masked login when key is already configured', () => {
    render(<SerpKeySettings {...defaultProps} has_key={true} masked_login="user@***" />);
    expect(screen.getByText(/user@\*\*\*/i)).toBeInTheDocument();
  });

  it('SerpKey validationInfoMap key set matches AiKey for drift safety', () => {
    const aiKeys = Object.keys(aiValidationInfoMap).sort();
    const serpKeys = Object.keys(serpValidationInfoMap).sort();
    // Both directions: every key in one map must exist in the other.
    expect(serpKeys).toEqual(aiKeys);
  });

  describe('ready-to-use next-step panel', () => {
    const setSites = async (sites: unknown[]) => {
      const { usePage } = await import('@inertiajs/react');
      vi.mocked(usePage).mockReturnValue({
        props: {
          auth: { user: { name: 'Test User', email: 'test@example.com' } },
          features: { billing: false, notifications: false },
          sites,
          limits: null,
          ai_defaults: { model: 'gpt-4o-mini', temperature: 0.7 },
        },
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
      } as any);
    };

    it('shows the ready panel and a drafts link when the key is connected and validated', async () => {
      await setSites([{ id: 7, name: 'My Site', domain: 'example.com' }]);
      render(
        <SerpKeySettings
          {...defaultProps}
          has_key={true}
          is_validated={true}
          masked_login="user@***"
          key_validation={{ status: 'valid', message: null, at: null, consecutiveFailures: 0 }}
        />,
      );
      expect(screen.getByText(/you.?re ready for serp analysis/i)).toBeInTheDocument();
      const cta = screen.getByRole('link', { name: /go to your drafts/i });
      expect(cta).toBeInTheDocument();
      expect(cta).toHaveAttribute('href', expect.stringContaining('/sites/7/pipeline'));
      // QA-001: tell users a draft must exist first so they don't hit an empty pipeline dead-end.
      expect(screen.getByText(/no draft yet\?/i)).toBeInTheDocument();
    });

    it('shows guidance instead of a broken link when the user has no site yet', async () => {
      await setSites([]);
      render(
        <SerpKeySettings
          {...defaultProps}
          has_key={true}
          is_validated={true}
          key_validation={{ status: 'valid', message: null, at: null, consecutiveFailures: 0 }}
        />,
      );
      expect(screen.getByText(/you.?re ready for serp analysis/i)).toBeInTheDocument();
      expect(screen.queryByRole('link', { name: /go to your drafts/i })).not.toBeInTheDocument();
      expect(screen.getByText(/add a site and generate a draft/i)).toBeInTheDocument();
    });

    it('does NOT show the ready panel when no key is configured', async () => {
      await setSites([{ id: 7, name: 'My Site', domain: 'example.com' }]);
      render(<SerpKeySettings {...defaultProps} />);
      expect(screen.queryByText(/you.?re ready for serp analysis/i)).not.toBeInTheDocument();
    });

    it('does NOT show the ready panel when the key is present but not validated', async () => {
      await setSites([{ id: 7, name: 'My Site', domain: 'example.com' }]);
      render(
        <SerpKeySettings
          {...defaultProps}
          has_key={true}
          masked_login="user@***"
          key_validation={{
            status: 'invalid_auth',
            message: 'Bad credentials',
            at: null,
            consecutiveFailures: 1,
          }}
        />,
      );
      expect(screen.queryByText(/you.?re ready for serp analysis/i)).not.toBeInTheDocument();
    });
  });

  // ============================================
  // SC-2: SERP password field disambiguation
  // ============================================

  it('password field label contains "DataForSEO" and "API Password" (SC-2)', () => {
    render(<SerpKeySettings {...defaultProps} />);
    // The label must contain both "DataForSEO" and "API Password" to be unambiguous.
    const label = screen.getByText(/dataforseo api password/i);
    expect(label).toBeInTheDocument();
  });

  it('password field helper text clarifies this is a DataForSEO account credential (SC-2)', () => {
    render(<SerpKeySettings {...defaultProps} />);
    expect(
      screen.getByText(/your dataforseo account login email and password/i),
    ).toBeInTheDocument();
  });

  // ============================================
  // R3UXA-008: prominent validation banner on page load
  // ============================================

  describe('R3UXA-008: validation banner', () => {
    it('shows a banner when key has invalid_auth status with the correct title', () => {
      render(
        <SerpKeySettings
          {...defaultProps}
          has_key={true}
          masked_login="user@***"
          key_validation={{ status: 'invalid_auth', message: 'Credentials rejected', at: null, consecutiveFailures: 2 }}
        />,
      );
      expect(screen.getByTestId('serp-key-validation-banner')).toBeInTheDocument();
      // destructive banner shows the rejection title
      expect(screen.getByText(/credentials were rejected/i)).toBeInTheDocument();
    });

    it('shows a warning banner when key has rate_limited status', () => {
      render(
        <SerpKeySettings
          {...defaultProps}
          has_key={true}
          masked_login="user@***"
          key_validation={{ status: 'rate_limited', message: 'Too many requests', at: null, consecutiveFailures: 1 }}
        />,
      );
      const banner = screen.getByTestId('serp-key-validation-banner');
      expect(banner).toBeInTheDocument();
    });

    it('does NOT show a banner when key is valid', () => {
      render(
        <SerpKeySettings
          {...defaultProps}
          has_key={true}
          masked_login="user@***"
          key_validation={{ status: 'valid', message: null, at: null, consecutiveFailures: 0 }}
        />,
      );
      expect(screen.queryByTestId('serp-key-validation-banner')).not.toBeInTheDocument();
    });

    it('does NOT show a banner when no key is configured', () => {
      render(<SerpKeySettings {...defaultProps} />);
      expect(screen.queryByTestId('serp-key-validation-banner')).not.toBeInTheDocument();
    });
  });

  // ============================================
  // R3FE-012: clear secret fields after successful save
  // ============================================

  describe('R3FE-012: secret fields cleared after successful credentials save', () => {
    it('calls reset("login", "password") in the post onSuccess callback', async () => {
      const user = userEvent.setup();
      render(<SerpKeySettings {...defaultProps} />);

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

      // R3FE-012: onSuccess callback must call reset('login', 'password')
      expect(mockPost).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({ onSuccess: expect.any(Function) }),
      );

      const { onSuccess } = (mockPost.mock.calls[0] as [string, { onSuccess: () => void }])[1];
      onSuccess();

      expect(mockReset).toHaveBeenCalledWith('login', 'password');
    });
  });

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

  describe('F5A11Y-002: server error ARIA linkage on credential inputs', () => {
    const errorFormReturn = {
      data: { provider: 'dataforseo', login: '', password: '' },
      setData: mockSetData,
      post: mockPost,
      reset: mockReset,
      processing: false,
      isDirty: false,
    };

    it('login input gets aria-invalid=true when a server error is present', () => {
      vi.mocked(useForm).mockReturnValueOnce({
        ...errorFormReturn,
        errors: { login: 'The login field is required.' },
      } as unknown as ReturnType<typeof useForm>);

      render(<SerpKeySettings {...defaultProps} />);

      const loginInput = screen.getByLabelText(/login/i);
      expect(loginInput).toHaveAttribute('aria-invalid', 'true');
    });

    it('login input is linked to the error message via aria-describedby', () => {
      vi.mocked(useForm).mockReturnValueOnce({
        ...errorFormReturn,
        errors: { login: 'The login field is required.' },
      } as unknown as ReturnType<typeof useForm>);

      render(<SerpKeySettings {...defaultProps} />);

      const loginInput = screen.getByLabelText(/login/i);
      expect(loginInput).toHaveAttribute('aria-describedby', 'login-error');
    });

    it('login error message rendered by InputError carries role=alert', () => {
      vi.mocked(useForm).mockReturnValueOnce({
        ...errorFormReturn,
        errors: { login: 'The login field is required.' },
      } as unknown as ReturnType<typeof useForm>);

      render(<SerpKeySettings {...defaultProps} />);

      // Multiple role="alert" elements exist (shadcn Alert primitives elsewhere in the
      // page also carry role="alert"). Use the id-tagged error element directly.
      const errorEl = document.getElementById('login-error');
      expect(errorEl).not.toBeNull();
      expect(errorEl).toHaveAttribute('role', 'alert');
      expect(errorEl).toHaveTextContent('The login field is required.');
    });

    it('login error <p> id matches the aria-describedby value on the input', () => {
      vi.mocked(useForm).mockReturnValueOnce({
        ...errorFormReturn,
        errors: { login: 'The login field is required.' },
      } as unknown as ReturnType<typeof useForm>);

      render(<SerpKeySettings {...defaultProps} />);

      const errorEl = document.getElementById('login-error');
      expect(errorEl).not.toBeNull();
      expect(errorEl).toHaveAttribute('id', 'login-error');
    });

    it('password input gets aria-invalid=true when a server error is present', () => {
      vi.mocked(useForm).mockReturnValueOnce({
        ...errorFormReturn,
        errors: { password: 'The password field is required.' },
      } as unknown as ReturnType<typeof useForm>);

      render(<SerpKeySettings {...defaultProps} />);

      const passwordInput = screen.getByLabelText(/dataforseo api password/i);
      expect(passwordInput).toHaveAttribute('aria-invalid', 'true');
    });

    it('password error message rendered by InputError carries role=alert', () => {
      vi.mocked(useForm).mockReturnValueOnce({
        ...errorFormReturn,
        errors: { password: 'The password field is required.' },
      } as unknown as ReturnType<typeof useForm>);

      render(<SerpKeySettings {...defaultProps} />);

      // Multiple role="alert" elements exist on the page; use the id-tagged element.
      const errorEl = document.getElementById('password-error');
      expect(errorEl).not.toBeNull();
      expect(errorEl).toHaveAttribute('role', 'alert');
      expect(errorEl).toHaveTextContent('The password field is required.');
    });
  });
});
