"""Frontend test generator for Prism.

Generates Vitest tests for React components and hooks.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from prism.generators.base import GeneratedFile, ModelGenerator
from prism.spec.stack import FileStrategy
from prism.utils.case_conversion import to_kebab_case

if TYPE_CHECKING:
    from prism.spec.model import ModelSpec


class FrontendTestGenerator(ModelGenerator):
    """Generator for frontend Vitest tests."""

    def __init__(self, *args, **kwargs) -> None:  # type: ignore[no-untyped-def]
        super().__init__(*args, **kwargs)
        frontend_base = Path(self.spec.generator.frontend_output)
        self.tests_path = frontend_base / self.spec.generator.frontend_tests_path

    @property
    def _auth_enabled_jwt(self) -> bool:
        """Check if JWT auth is enabled."""
        return self.spec.auth.enabled and self.spec.auth.preset == "jwt"

    def generate_shared_files(self) -> list[GeneratedFile]:
        """Generate shared test setup files."""
        files = [
            self._generate_test_setup(),
            self._generate_test_utils(),
        ]

        if self._auth_enabled_jwt:
            files.extend(self._generate_auth_tests())

        return files

    def generate_model_files(self, model: ModelSpec) -> list[GeneratedFile]:
        """Generate tests for a single model."""
        if not model.frontend.enabled:
            return []

        files = []

        if self.spec.testing.generate_component_tests:
            files.append(self._generate_component_tests(model))

        if self.spec.testing.generate_hook_tests:
            files.append(self._generate_hook_tests(model))

        return files

    def generate_index_files(self) -> list[GeneratedFile]:
        """No index files for tests."""
        return []

    def _generate_test_setup(self) -> GeneratedFile:
        """Generate Vitest setup file."""
        content = """/**
 * Vitest test setup.
 *
 * ⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
 */

import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';

// Extend Vitest's expect with Testing Library matchers
expect.extend(matchers);

// Mock URL.createObjectURL for libraries that use it (e.g. maplibre-gl) in jsdom
if (typeof window !== 'undefined') {
  if (!window.URL.createObjectURL) {
    window.URL.createObjectURL = () => '';
  }
  if (!window.URL.revokeObjectURL) {
    window.URL.revokeObjectURL = () => {};
  }
}

// Cleanup after each test
afterEach(() => {
  cleanup();
});
"""

        return GeneratedFile(
            path=self.tests_path / "setup.ts",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description="Vitest setup",
        )

    def _generate_test_utils(self) -> GeneratedFile:
        """Generate test utilities."""
        content = """/**
 * Test utilities and helpers.
 *
 * ⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
 */

import React, { type ReactElement } from 'react';
import { render, type RenderOptions } from '@testing-library/react';
import { Provider, createClient, cacheExchange, fetchExchange } from 'urql';
import { WidgetProvider } from '../prism/widgets';

// Create a mock urql client for testing
const mockClient = createClient({
  url: 'http://localhost:8000/graphql',
  exchanges: [cacheExchange, fetchExchange],
  // Disable suspense to prevent test hangs
  suspense: false,
});

// Add providers for testing
interface WrapperProps {
  children: React.ReactNode;
}

function AllTheProviders({ children }: WrapperProps): ReactElement {
  return (
    <Provider value={mockClient}>
      <WidgetProvider>
        {children}
      </WidgetProvider>
    </Provider>
  );
}

/**
 * Custom render function that wraps components with providers.
 */
function customRender(
  ui: ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) {
  return render(ui, { wrapper: AllTheProviders, ...options });
}

// Re-export everything from Testing Library
export * from '@testing-library/react';

// Override render with custom render
export { customRender as render };

// Export mock client for advanced test cases
export { mockClient };

/**
 * Create mock data for testing.
 */
export function createMockData<T>(overrides: Partial<T> = {}): T {
  return {
    id: 1,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    ...overrides,
  } as T;
}

/**
 * Wait for async operations.
 */
export async function waitForAsync() {
  await new Promise((resolve) => setTimeout(resolve, 0));
}
"""

        return GeneratedFile(
            path=self.tests_path / "utils.tsx",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description="Test utilities",
        )

    def _generate_component_tests(self, model: ModelSpec) -> GeneratedFile:
        """Generate component tests for a model."""
        kebab_name = to_kebab_case(model.name)

        # Check which operations are enabled
        ops = model.frontend.operations
        has_create = ops.create
        has_update = ops.update
        has_delete = ops.delete

        # Check which components are actually generated
        has_form = model.frontend.generate_form
        has_detail = model.frontend.generate_detail_view

        # Build imports based on what components exist
        imports = [f"{model.name}Table"]  # Table always exists
        if has_detail:
            imports.append(f"{model.name}Detail")
        if has_form and (has_create or has_update):
            imports.insert(0, f"{model.name}Form")

        import_lines = "\n".join(
            f"import {{ {imp} }} from '../../components/{kebab_name}/{imp}';" for imp in imports
        )

        # Build form tests only if form component exists and create or update is enabled
        form_tests = ""
        if has_form and (has_create or has_update):
            form_tests = f"""
describe('{model.name}Form', () => {{
  it('renders form fields', () => {{
    const onSubmit = vi.fn();

    render(<{model.name}Form onSubmit={{onSubmit}} />);

    // Check that form renders by looking for the submit button
    expect(screen.getByRole('button', {{ name: /save/i }})).toBeInTheDocument();
  }});

  it('calls onSubmit with form data', async () => {{
    const onSubmit = vi.fn();

    render(<{model.name}Form onSubmit={{onSubmit}} />);

    // Fill in form fields and submit
    const submitButton = screen.getByRole('button', {{ name: /save/i }});
    fireEvent.click(submitButton);

    await waitFor(() => {{
      // onSubmit should be called (may fail validation)
    }});
  }});

  it('displays loading state', () => {{
    const onSubmit = vi.fn();

    render(<{model.name}Form onSubmit={{onSubmit}} loading={{true}} />);

    const submitButton = screen.getByRole('button', {{ name: /saving/i }});
    expect(submitButton).toBeDisabled();
  }});
}});
"""

        # Build edit button test only if update is enabled
        edit_table_test = ""
        if has_update:
            edit_table_test = f"""
  it('calls onEdit when edit button is clicked', () => {{
    const data = [mock{model.name}];
    const onEdit = vi.fn();

    render(<{model.name}Table data={{data}} onEdit={{onEdit}} />);

    const editButton = screen.getByRole('button', {{ name: /edit/i }});
    fireEvent.click(editButton);

    expect(onEdit).toHaveBeenCalledWith(mock{model.name});
  }});
"""

        # Build delete button test only if delete is enabled
        delete_table_test = ""
        if has_delete:
            delete_table_test = f"""
  it('calls onDelete when delete button is clicked', () => {{
    const data = [mock{model.name}];
    const onDelete = vi.fn();

    render(<{model.name}Table data={{data}} onDelete={{onDelete}} />);

    const deleteButton = screen.getByRole('button', {{ name: /delete/i }});
    fireEvent.click(deleteButton);

    expect(onDelete).toHaveBeenCalledWith(mock{model.name});
  }});
"""

        # Build edit button test in detail only if update is enabled
        edit_detail_test = ""
        if has_update:
            edit_detail_test = f"""
  it('calls onEdit when edit button is clicked', () => {{
    const onEdit = vi.fn();

    render(<{model.name}Detail data={{mock{model.name}}} onEdit={{onEdit}} />);

    const editButton = screen.getByRole('button', {{ name: /edit/i }});
    fireEvent.click(editButton);

    expect(onEdit).toHaveBeenCalled();
  }});
"""

        # Build detail tests only if detail component exists
        detail_tests = ""
        if has_detail:
            detail_tests = f"""
describe('{model.name}Detail', () => {{
  it('renders detail view', () => {{
    render(<{model.name}Detail data={{mock{model.name}}} />);

    expect(screen.getByText(`{model.name} #${{mock{model.name}.id}}`)).toBeInTheDocument();
  }});
{edit_detail_test}
  it('calls onBack when back button is clicked', () => {{
    const onBack = vi.fn();

    render(<{model.name}Detail data={{mock{model.name}}} onBack={{onBack}} />);

    const backButton = screen.getByRole('button', {{ name: /back/i }});
    fireEvent.click(backButton);

    expect(onBack).toHaveBeenCalled();
  }});
}});
"""

        # Build timestamp fields only if model has timestamps
        timestamp_fields = ""
        if model.timestamps:
            timestamp_fields = """  createdAt: '2024-01-01T00:00:00Z',
  updatedAt: '2024-01-01T00:00:00Z',"""

        content = f"""/**
 * Component tests for {model.name}.
 *
 * ⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
 */

import {{ describe, it, expect, vi }} from 'vitest';
import {{ render, screen, fireEvent, waitFor }} from '../utils';
{import_lines}
import type {{ {model.name} }} from '../../types/generated';

// Mock data
const mock{model.name}: {model.name} = {{
  id: 1,
{self._build_mock_data(model)}
{timestamp_fields}
}};
{form_tests}
describe('{model.name}Table', () => {{
  it('renders table with data', () => {{
    const data = [mock{model.name}];

    render(<{model.name}Table data={{data}} />);

    expect(screen.getByRole('table')).toBeInTheDocument();
  }});

  it('shows empty state when no data', () => {{
    render(<{model.name}Table data={{[]}} />);

    expect(screen.getByText(/no.*found/i)).toBeInTheDocument();
  }});

  it('renders table rows with data', () => {{
    const data = [mock{model.name}];

    render(<{model.name}Table data={{data}} />);

    // Check that table body has rows
    expect(screen.getAllByRole('row').length).toBeGreaterThan(1);
  }});
{edit_table_test}{delete_table_test}}});
{detail_tests}"""

        return GeneratedFile(
            path=self.tests_path / "components" / f"{model.name}.test.tsx",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description=f"Component tests for {model.name}",
        )

    def _generate_hook_tests(self, model: ModelSpec) -> GeneratedFile:
        """Generate hook tests for a model."""
        # Check which operations are enabled (use API-level ops, not frontend defaults)
        if model.frontend.api_style == "graphql":
            ops = model.graphql.operations
        else:
            ops = model.frontend.operations
        has_create = ops.create
        has_update = ops.update
        has_delete = ops.delete
        has_any_mutation = has_create or has_update or has_delete

        # Build mutation hook import
        mutation_import = ""
        if has_any_mutation:
            mutation_import = f", use{model.name}Mutations"

        # Build mutation tests
        mutation_tests = ""
        if has_any_mutation:
            create_test = ""
            if has_create:
                create_test = f"""
  it('provides create function', () => {{
    const {{ result }} = renderHook(() => use{model.name}Mutations(), {{ wrapper: Wrapper }});

    expect(result.current.create).toBeInstanceOf(Function);
  }});
"""

            update_test = ""
            if has_update:
                update_test = f"""
  it('provides update function', () => {{
    const {{ result }} = renderHook(() => use{model.name}Mutations(), {{ wrapper: Wrapper }});

    expect(result.current.update).toBeInstanceOf(Function);
  }});
"""

            remove_test = ""
            if has_delete:
                remove_test = f"""
  it('provides remove function', () => {{
    const {{ result }} = renderHook(() => use{model.name}Mutations(), {{ wrapper: Wrapper }});

    expect(result.current.remove).toBeInstanceOf(Function);
  }});
"""

            mutation_tests = f"""
describe('use{model.name}Mutations', () => {{
  beforeEach(() => {{
    vi.clearAllMocks();
  }});
{create_test}{update_test}{remove_test}
  it('tracks loading state', () => {{
    const {{ result }} = renderHook(() => use{model.name}Mutations(), {{ wrapper: Wrapper }});

    expect(result.current.loading).toBe(false);
  }});
}});
"""

        content = f"""/**
 * Hook tests for {model.name}.
 *
 * ⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
 */

import React from 'react';
import {{ describe, it, expect, vi, beforeEach }} from 'vitest';
import {{ renderHook, waitFor }} from '@testing-library/react';
import {{ Provider, createClient, cacheExchange, fetchExchange }} from 'urql';
import {{ use{model.name}, use{model.name}List{mutation_import} }} from '../../hooks/use{model.name}';

// Create a mock urql client
const mockClient = createClient({{
  url: 'http://localhost:8000/graphql',
  exchanges: [cacheExchange, fetchExchange],
}});

// Wrapper component with urql Provider
function Wrapper({{ children }}: {{ children: React.ReactNode }}) {{
  return <Provider value={{mockClient}}>{{children}}</Provider>;
}}

describe('use{model.name}', () => {{
  beforeEach(() => {{
    vi.clearAllMocks();
  }});

  it('returns loading state initially when id is provided', () => {{
    const {{ result }} = renderHook(() => use{model.name}(1), {{ wrapper: Wrapper }});

    // Hook should return loading or data state
    expect(result.current).toBeDefined();
  }});

  it('handles null id', () => {{
    const {{ result }} = renderHook(() => use{model.name}(null), {{ wrapper: Wrapper }});

    // Should handle null id gracefully
    expect(result.current).toBeDefined();
  }});
}});

describe('use{model.name}List', () => {{
  beforeEach(() => {{
    vi.clearAllMocks();
  }});

  it('returns initial state', () => {{
    const {{ result }} = renderHook(() => use{model.name}List(), {{ wrapper: Wrapper }});

    expect(result.current).toBeDefined();
    expect(result.current.data).toBeDefined();
  }});

  it('accepts pagination options', () => {{
    const {{ result }} = renderHook(
      () => use{model.name}List({{ page: 2, pageSize: 10 }}),
      {{ wrapper: Wrapper }}
    );

    expect(result.current).toBeDefined();
  }});
}});
{mutation_tests}"""

        return GeneratedFile(
            path=self.tests_path / "hooks" / f"use{model.name}.test.tsx",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description=f"Hook tests for {model.name}",
        )

    # ── Auth test generation ───────────────────────────────────────

    def _generate_auth_tests(self) -> list[GeneratedFile]:
        """Generate all auth test files when JWT auth is enabled."""
        files = [
            self._generate_auth_api_tests(),
            self._generate_auth_context_tests(),
            self._generate_login_form_tests(),
            self._generate_signup_form_tests(),
            self._generate_protected_route_tests(),
        ]
        return files

    def _generate_auth_api_tests(self) -> GeneratedFile:
        """Generate tests for auth API client functions."""
        auth = self.spec.auth

        mfa_tests = ""
        if auth.mfa_enabled:
            mfa_tests = """
  it('calls loginMFA', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ user: { id: 1, email: 'a@b.com' } }),
    } as Response);

    const result = await authApi.loginMFA('a@b.com', '123456');
    expect(global.fetch).toHaveBeenCalledWith('/api/auth/login/mfa', expect.objectContaining({
      method: 'POST',
    }));
    expect(result.user).toBeDefined();
  });

  it('calls setupMFA', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ totp_uri: 'otpauth://...', secret: 'abc' }),
    } as Response);

    const result = await authApi.setupMFA();
    expect(global.fetch).toHaveBeenCalledWith('/api/auth/mfa/setup', expect.objectContaining({
      method: 'POST',
    }));
    expect(result.secret).toBeDefined();
  });

  it('calls verifyMFASetup', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ message: 'MFA enabled' }),
    } as Response);

    await authApi.verifyMFASetup('123456');
    expect(global.fetch).toHaveBeenCalledWith('/api/auth/mfa/verify-setup', expect.objectContaining({
      method: 'POST',
    }));
  });

  it('calls disableMFA', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ message: 'MFA disabled' }),
    } as Response);

    await authApi.disableMFA('password');
    expect(global.fetch).toHaveBeenCalledWith('/api/auth/mfa/disable', expect.objectContaining({
      method: 'POST',
    }));
  });
"""

        verify_email_test = ""
        if auth.email_verification:
            verify_email_test = """
  it('calls verifyEmail', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ message: 'Email verified' }),
    } as Response);

    const result = await authApi.verifyEmail('token123');
    expect(global.fetch).toHaveBeenCalledWith('/api/auth/verify-email', expect.objectContaining({
      method: 'POST',
    }));
    expect(result.message).toBe('Email verified');
  });
"""

        password_reset_tests = ""
        if auth.password_reset:
            password_reset_tests = """
  it('calls forgotPassword', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ message: 'ok' }),
    } as Response);

    await authApi.forgotPassword('a@b.com');
    expect(global.fetch).toHaveBeenCalledWith('/api/auth/forgot-password', expect.objectContaining({
      method: 'POST',
    }));
  });

  it('calls resetPassword', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ message: 'ok' }),
    } as Response);

    await authApi.resetPassword('token', 'NewPass1');
    expect(global.fetch).toHaveBeenCalledWith('/api/auth/reset-password', expect.objectContaining({
      method: 'POST',
    }));
  });
"""

        content = f"""/**
 * Tests for auth API client.
 *
 * ⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
 */

import {{ describe, it, expect, vi, beforeEach }} from 'vitest';
import * as authApi from '../../lib/authApi';

describe('authApi', () => {{
  beforeEach(() => {{
    vi.restoreAllMocks();
  }});

  it('calls signup', async () => {{
    global.fetch = vi.fn().mockResolvedValue({{
      ok: true,
      json: () => Promise.resolve({{ message: 'Check your email' }}),
    }} as Response);

    const result = await authApi.signup('a@b.com', 'user1', 'StrongPass1');
    expect(global.fetch).toHaveBeenCalledWith('/api/auth/signup', expect.objectContaining({{
      method: 'POST',
    }}));
    expect(result.message).toBeDefined();
  }});

  it('calls login', async () => {{
    global.fetch = vi.fn().mockResolvedValue({{
      ok: true,
      json: () => Promise.resolve({{ requires_mfa: false, user: {{ id: 1 }} }}),
    }} as Response);

    const result = await authApi.login('a@b.com', 'StrongPass1');
    expect(global.fetch).toHaveBeenCalledWith('/api/auth/login', expect.objectContaining({{
      method: 'POST',
    }}));
    expect(result.requires_mfa).toBe(false);
  }});
{mfa_tests}{verify_email_test}{password_reset_tests}
  it('throws on error response', async () => {{
    global.fetch = vi.fn().mockResolvedValue({{
      ok: false,
      json: () => Promise.resolve({{ detail: 'Bad request' }}),
    }} as Response);

    await expect(authApi.login('a@b.com', 'bad')).rejects.toThrow();
  }});
}});
"""

        return GeneratedFile(
            path=self.tests_path / "auth" / "authApi.test.ts",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description="Auth API client tests",
        )

    def _generate_auth_context_tests(self) -> GeneratedFile:
        """Generate tests for AuthContext/AuthProvider."""
        content = """/**
 * Tests for AuthContext and AuthProvider.
 *
 * ⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
 */

import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, act } from '@testing-library/react';
import { AuthProvider, useAuth } from '../../contexts/AuthContext';

function TestConsumer() {
  const { user, loading } = useAuth();
  if (loading) return <div>Loading</div>;
  if (user) return <div>Authenticated: {user.email}</div>;
  return <div>Not authenticated</div>;
}

describe('AuthContext', () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  it('shows authenticated user on success', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 1, email: 'test@example.com', roles: ['user'] }),
    } as Response);

    render(
      <AuthProvider>
        <TestConsumer />
      </AuthProvider>
    );

    await waitFor(() => {
      expect(screen.getByText(/Authenticated/)).toBeInTheDocument();
    });
  });

  it('shows unauthenticated on 401', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      json: () => Promise.resolve({ detail: 'Not authenticated' }),
    } as Response);

    render(
      <AuthProvider>
        <TestConsumer />
      </AuthProvider>
    );

    await waitFor(() => {
      expect(screen.getByText('Not authenticated')).toBeInTheDocument();
    });
  });

  it('provides logout function', async () => {
    global.fetch = vi.fn()
      .mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ id: 1, email: 'test@example.com', roles: ['user'] }),
      } as Response)
      .mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ message: 'Logged out' }),
      } as Response);

    function LogoutConsumer() {
      const { user, logout, loading } = useAuth();
      if (loading) return <div>Loading</div>;
      return (
        <div>
          <span>{user ? 'Logged in' : 'Logged out'}</span>
          <button onClick={logout}>Logout</button>
        </div>
      );
    }

    render(
      <AuthProvider>
        <LogoutConsumer />
      </AuthProvider>
    );

    await waitFor(() => {
      expect(screen.getByText('Logged in')).toBeInTheDocument();
    });
  });
});
"""

        return GeneratedFile(
            path=self.tests_path / "auth" / "AuthContext.test.tsx",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description="AuthContext tests",
        )

    def _generate_login_form_tests(self) -> GeneratedFile:
        """Generate tests for LoginForm component."""
        auth = self.spec.auth

        mfa_test = ""
        if auth.mfa_enabled:
            mfa_test = """
  it('shows MFA step when required', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ requires_mfa: true, user: null }),
    } as Response);

    render(
      <MemoryRouter>
        <AuthProvider>
          <LoginForm />
        </AuthProvider>
      </MemoryRouter>
    );

    await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
    await userEvent.type(screen.getByLabelText(/password/i), 'StrongPass1');
    await userEvent.click(screen.getByRole('button', { name: /sign in/i }));

    await waitFor(() => {
      expect(screen.getByLabelText(/code/i)).toBeInTheDocument();
    });
  });
"""

        github_test = ""
        if auth.has_oauth_provider("github"):
            github_test = """
  it('shows GitHub login link', () => {
    render(
      <MemoryRouter>
        <AuthProvider>
          <LoginForm />
        </AuthProvider>
      </MemoryRouter>
    );

    expect(screen.getByText(/github/i)).toBeInTheDocument();
  });
"""

        content = f"""/**
 * Tests for LoginForm component.
 *
 * ⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
 */

import React from 'react';
import {{ describe, it, expect, vi, beforeEach }} from 'vitest';
import {{ render, screen, waitFor }} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {{ MemoryRouter }} from 'react-router-dom';
import {{ AuthProvider }} from '../../contexts/AuthContext';
import {{ LoginForm }} from '../../components/auth/LoginForm';

describe('LoginForm', () => {{
  beforeEach(() => {{
    vi.restoreAllMocks();
    // Mock /api/auth/me to return unauthenticated
    global.fetch = vi.fn().mockResolvedValue({{
      ok: false,
      status: 401,
      json: () => Promise.resolve({{ detail: 'Not authenticated' }}),
    }} as Response);
  }});

  it('renders email and password fields', () => {{
    render(
      <MemoryRouter>
        <AuthProvider>
          <LoginForm />
        </AuthProvider>
      </MemoryRouter>
    );

    expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
  }});

  it('submits credentials', async () => {{
    const loginFetch = vi.fn().mockResolvedValue({{
      ok: true,
      json: () => Promise.resolve({{ requires_mfa: false, user: {{ id: 1, email: 'a@b.com' }} }}),
    }} as Response);

    // First call: /api/auth/me (unauthenticated), second: login
    global.fetch = vi.fn()
      .mockResolvedValueOnce({{
        ok: false,
        status: 401,
        json: () => Promise.resolve({{ detail: 'Not authenticated' }}),
      }} as Response)
      .mockImplementation(loginFetch);

    render(
      <MemoryRouter>
        <AuthProvider>
          <LoginForm />
        </AuthProvider>
      </MemoryRouter>
    );

    await waitFor(() => {{
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    }});

    await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
    await userEvent.type(screen.getByLabelText(/password/i), 'StrongPass1');
    await userEvent.click(screen.getByRole('button', {{ name: /sign in/i }}));

    await waitFor(() => {{
      expect(loginFetch).toHaveBeenCalled();
    }});
  }});

  it('shows error on failure', async () => {{
    global.fetch = vi.fn()
      .mockResolvedValueOnce({{
        ok: false,
        status: 401,
        json: () => Promise.resolve({{ detail: 'Not authenticated' }}),
      }} as Response)
      .mockResolvedValueOnce({{
        ok: false,
        json: () => Promise.resolve({{ detail: 'Invalid credentials' }}),
      }} as Response);

    render(
      <MemoryRouter>
        <AuthProvider>
          <LoginForm />
        </AuthProvider>
      </MemoryRouter>
    );

    await waitFor(() => {{
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    }});

    await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
    await userEvent.type(screen.getByLabelText(/password/i), 'wrong');
    await userEvent.click(screen.getByRole('button', {{ name: /sign in/i }}));

    await waitFor(() => {{
      expect(screen.getByText(/invalid|error|failed/i)).toBeInTheDocument();
    }});
  }});
{mfa_test}{github_test}}});
"""

        return GeneratedFile(
            path=self.tests_path / "auth" / "LoginForm.test.tsx",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description="LoginForm tests",
        )

    def _generate_signup_form_tests(self) -> GeneratedFile:
        """Generate tests for SignupForm component."""
        content = """/**
 * Tests for SignupForm component.
 *
 * ⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
 */

import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { AuthProvider } from '../../contexts/AuthContext';
import { SignupForm } from '../../components/auth/SignupForm';

describe('SignupForm', () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      json: () => Promise.resolve({ detail: 'Not authenticated' }),
    } as Response);
  });

  it('renders signup fields', () => {
    render(
      <MemoryRouter>
        <AuthProvider>
          <SignupForm />
        </AuthProvider>
      </MemoryRouter>
    );

    expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
  });

  it('shows password mismatch error', async () => {
    render(
      <MemoryRouter>
        <AuthProvider>
          <SignupForm />
        </AuthProvider>
      </MemoryRouter>
    );

    await waitFor(() => {
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    });

    await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
    await userEvent.type(screen.getByLabelText(/username/i), 'testuser');
    const passwordFields = screen.getAllByLabelText(/password/i);
    await userEvent.type(passwordFields[0], 'StrongPass1');
    if (passwordFields.length > 1) {
      await userEvent.type(passwordFields[1], 'Different1');
    }
    await userEvent.click(screen.getByRole('button', { name: /sign up|create/i }));

    await waitFor(() => {
      const errorEl = screen.queryByText(/match|mismatch/i);
      // Only assert if confirm password field exists
      if (passwordFields.length > 1) {
        expect(errorEl).toBeInTheDocument();
      }
    });
  });

  it('submits signup', async () => {
    const signupFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ message: 'Check your email' }),
    } as Response);

    global.fetch = vi.fn()
      .mockResolvedValueOnce({
        ok: false,
        status: 401,
        json: () => Promise.resolve({ detail: 'Not authenticated' }),
      } as Response)
      .mockImplementation(signupFetch);

    render(
      <MemoryRouter>
        <AuthProvider>
          <SignupForm />
        </AuthProvider>
      </MemoryRouter>
    );

    await waitFor(() => {
      expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    });

    await userEvent.type(screen.getByLabelText(/email/i), 'new@example.com');
    await userEvent.type(screen.getByLabelText(/username/i), 'newuser');
    const passwordFields = screen.getAllByLabelText(/password/i);
    await userEvent.type(passwordFields[0], 'StrongPass1');
    if (passwordFields.length > 1) {
      await userEvent.type(passwordFields[1], 'StrongPass1');
    }
    await userEvent.click(screen.getByRole('button', { name: /sign up|create/i }));

    await waitFor(() => {
      expect(signupFetch).toHaveBeenCalled();
    });
  });
});
"""

        return GeneratedFile(
            path=self.tests_path / "auth" / "SignupForm.test.tsx",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description="SignupForm tests",
        )

    def _generate_protected_route_tests(self) -> GeneratedFile:
        """Generate tests for ProtectedRoute component."""
        content = """/**
 * Tests for ProtectedRoute component.
 *
 * ⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
 */

import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { AuthProvider } from '../../contexts/AuthContext';
import { ProtectedRoute } from '../../components/auth/ProtectedRoute';

describe('ProtectedRoute', () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  it('renders children when authenticated', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 1, email: 'test@example.com', roles: ['user'] }),
    } as Response);

    render(
      <MemoryRouter initialEntries={['/protected']}>
        <AuthProvider>
          <Routes>
            <Route path="/protected" element={
              <ProtectedRoute>
                <div>Protected Content</div>
              </ProtectedRoute>
            } />
            <Route path="/login" element={<div>Login Page</div>} />
          </Routes>
        </AuthProvider>
      </MemoryRouter>
    );

    await waitFor(() => {
      expect(screen.getByText('Protected Content')).toBeInTheDocument();
    });
  });

  it('redirects when unauthenticated', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      json: () => Promise.resolve({ detail: 'Not authenticated' }),
    } as Response);

    render(
      <MemoryRouter initialEntries={['/protected']}>
        <AuthProvider>
          <Routes>
            <Route path="/protected" element={
              <ProtectedRoute>
                <div>Protected Content</div>
              </ProtectedRoute>
            } />
            <Route path="/login" element={<div>Login Page</div>} />
          </Routes>
        </AuthProvider>
      </MemoryRouter>
    );

    await waitFor(() => {
      expect(screen.getByText('Login Page')).toBeInTheDocument();
    });
  });

  it('blocks without required role', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 1, email: 'test@example.com', roles: ['user'] }),
    } as Response);

    render(
      <MemoryRouter initialEntries={['/admin']}>
        <AuthProvider>
          <Routes>
            <Route path="/admin" element={
              <ProtectedRoute requiredRole="admin">
                <div>Admin Content</div>
              </ProtectedRoute>
            } />
            <Route path="/login" element={<div>Login Page</div>} />
          </Routes>
        </AuthProvider>
      </MemoryRouter>
    );

    await waitFor(() => {
      // Should not show admin content
      expect(screen.queryByText('Admin Content')).not.toBeInTheDocument();
    });
  });

  it('allows with required role', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 1, email: 'test@example.com', roles: ['admin'] }),
    } as Response);

    render(
      <MemoryRouter initialEntries={['/admin']}>
        <AuthProvider>
          <Routes>
            <Route path="/admin" element={
              <ProtectedRoute requiredRole="admin">
                <div>Admin Content</div>
              </ProtectedRoute>
            } />
            <Route path="/login" element={<div>Login Page</div>} />
          </Routes>
        </AuthProvider>
      </MemoryRouter>
    );

    await waitFor(() => {
      expect(screen.getByText('Admin Content')).toBeInTheDocument();
    });
  });
});
"""

        return GeneratedFile(
            path=self.tests_path / "auth" / "ProtectedRoute.test.tsx",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description="ProtectedRoute tests",
        )

    def _build_mock_data(self, model: ModelSpec) -> str:
        """Build mock data fields."""
        lines = []

        for field in model.fields:
            value = self._get_mock_value(field)
            camel_name = (
                field.name[0].lower() + field.name.replace("_", " ").title().replace(" ", "")[1:]
            )
            lines.append(f"  {camel_name}: {value},")

        return "\n".join(lines)

    def _get_mock_value(self, field) -> str:
        """Get a mock value for a field."""
        from prism.spec.fields import FieldType

        if field.type == FieldType.STRING:
            return "'test string'"
        elif field.type == FieldType.TEXT:
            return "'Test text content'"
        elif field.type == FieldType.INTEGER:
            return "42"
        elif field.type in (FieldType.FLOAT, FieldType.DECIMAL):
            return "3.14"
        elif field.type == FieldType.BOOLEAN:
            return "true"
        elif field.type == FieldType.DATETIME:
            return "'2024-01-01T00:00:00Z'"
        elif field.type == FieldType.DATE:
            return "'2024-01-01'"
        elif field.type == FieldType.TIME:
            return "'12:00:00'"
        elif field.type == FieldType.ENUM and field.enum_values:
            return f"'{field.enum_values[0]}'"
        elif field.type == FieldType.FOREIGN_KEY:
            return "1"
        elif field.type == FieldType.JSON:
            # Handle typed JSON arrays vs untyped JSON objects
            if field.json_item_type:
                # Typed array
                if field.json_item_type in ("str", "string"):
                    return "['test']"
                elif field.json_item_type in ("int", "integer", "float", "number"):
                    return "[1, 2, 3]"
                elif field.json_item_type in ("bool", "boolean"):
                    return "[true, false]"
                else:
                    return "['test']"
            else:
                # Untyped JSON object
                return "{ test: 'value' }"
        elif field.type == FieldType.UUID:
            return "'00000000-0000-0000-0000-000000000001'"
        return "'test'"


__all__ = ["FrontendTestGenerator"]
