/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import React from 'react';
import { renderWithProviders } from '../../../test-utils/render.js';
import { waitFor } from '../../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  SearchableList,
  type SearchableListProps,
  type SearchListState,
  type GenericListItem,
} from './SearchableList.js';
import { useTextBuffer } from './text-buffer.js';

const useMockSearch = (props: {
  items: GenericListItem[];
  initialQuery?: string;
  onSearch?: (query: string) => void;
}): SearchListState<GenericListItem> => {
  const { onSearch, items, initialQuery = '' } = props;
  const [text, setText] = React.useState(initialQuery);
  const filteredItems = React.useMemo(
    () =>
      items.filter((item: GenericListItem) =>
        item.label.toLowerCase().includes(text.toLowerCase()),
      ),
    [items, text],
  );

  React.useEffect(() => {
    onSearch?.(text);
  }, [text, onSearch]);

  const searchBuffer = useTextBuffer({
    initialText: text,
    onChange: setText,
    viewport: { width: 100, height: 1 },
    singleLine: true,
  });

  return {
    filteredItems,
    searchBuffer,
    searchQuery: text,
    setSearchQuery: setText,
    maxLabelWidth: 10,
  };
};

const mockItems: GenericListItem[] = [
  {
    key: 'item-1',
    label: 'Item One',
    description: 'Description for item one',
  },
  {
    key: 'item-2',
    label: 'Item Two',
    description: 'Description for item two',
  },
  {
    key: 'item-3',
    label: 'Item Three',
    description: 'Description for item three',
  },
];

describe('SearchableList', () => {
  let mockOnSelect: ReturnType<typeof vi.fn>;
  let mockOnClose: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockOnSelect = vi.fn();
    mockOnClose = vi.fn();
  });

  const renderList = async (
    props: Partial<SearchableListProps<GenericListItem>> = {},
  ) => {
    const defaultProps: SearchableListProps<GenericListItem> = {
      title: 'Test List',
      items: mockItems,
      onSelect: mockOnSelect,
      onClose: mockOnClose,
      useSearch: useMockSearch,
      ...props,
    };

    return renderWithProviders(<SearchableList {...defaultProps} />);
  };

  it('should render all items initially', async () => {
    const { lastFrame } = await renderList();
    const frame = lastFrame();

    expect(frame).toContain('Test List');

    expect(frame).toContain('Item One');
    expect(frame).toContain('Item Two');
    expect(frame).toContain('Item Three');

    expect(frame).toContain('Description for item one');
  });

  it('should reset selection to top when items change if resetSelectionOnItemsChange is true', async () => {
    const { lastFrame, stdin } = await renderList({
      resetSelectionOnItemsChange: true,
    });

    await React.act(async () => {
      stdin.write('\u001B[B'); // Down arrow
    });

    await waitFor(() => {
      const frame = lastFrame();
      expect(frame).toContain('● Item Two');
    });
    expect(lastFrame()).toMatchSnapshot();

    await React.act(async () => {
      stdin.write('One');
    });

    await waitFor(() => {
      const frame = lastFrame();
      expect(frame).toContain('Item One');
      expect(frame).not.toContain('Item Two');
    });
    expect(lastFrame()).toMatchSnapshot();

    await React.act(async () => {
      // Backspace "One" (3 chars)
      stdin.write('\u007F\u007F\u007F');
    });

    await waitFor(() => {
      const frame = lastFrame();
      expect(frame).toContain('Item Two');
      expect(frame).toContain('● Item One');
      expect(frame).not.toContain('● Item Two');
    });
    expect(lastFrame()).toMatchSnapshot();
  });

  it('should filter items based on search query', async () => {
    const { lastFrame, stdin } = await renderList();

    await React.act(async () => {
      stdin.write('Two');
    });

    await waitFor(() => {
      const frame = lastFrame();
      expect(frame).toContain('Item Two');
      expect(frame).not.toContain('Item One');
      expect(frame).not.toContain('Item Three');
    });
  });

  it('should show "No items found." when no items match', async () => {
    const { lastFrame, stdin } = await renderList();

    await React.act(async () => {
      stdin.write('xyz123');
    });

    await waitFor(() => {
      const frame = lastFrame();
      expect(frame).toContain('No items found.');
    });
  });

  it('should handle selection with Enter', async () => {
    const { stdin } = await renderList();

    await React.act(async () => {
      stdin.write('\r'); // Enter
    });

    await waitFor(() => {
      expect(mockOnSelect).toHaveBeenCalledWith(mockItems[0]);
    });
  });

  it('should handle navigation and selection', async () => {
    const { stdin } = await renderList();

    await React.act(async () => {
      stdin.write('\u001B[B'); // Down arrow
    });

    await React.act(async () => {
      stdin.write('\r'); // Enter
    });

    await waitFor(() => {
      expect(mockOnSelect).toHaveBeenCalledWith(mockItems[1]);
    });
  });

  it('should handle close with Esc', async () => {
    const { stdin } = await renderList();

    await React.act(async () => {
      stdin.write('\u001B'); // Esc
    });

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

  it('should match snapshot', async () => {
    const { lastFrame } = await renderList();
    expect(lastFrame()).toMatchSnapshot();
  });
});
