"""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

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

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

// 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
        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}",
        )

    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"]
