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

import { createRef, type FormEvent } from 'react';

import {
  TableFilterBar,
  type DateRangeConfig,
  type FilterGroup,
  type SearchConfig,
  type SliderConfig,
  type SortConfig,
} from './TableFilterBar';

const TYPE_FILTER: FilterGroup = {
  key: 'post_type',
  label: 'Type',
  value: null,
  allLabel: 'All types',
  allCount: 351,
  options: [
    { value: 'post', label: 'post', count: 300 },
    { value: 'page', label: 'page', count: 51 },
  ],
};

const STATUS_FILTER: FilterGroup = {
  key: 'post_status',
  label: 'Status',
  value: null,
  allLabel: 'All statuses',
  options: [
    { value: 'publish', label: 'publish', count: 200 },
    { value: 'draft', label: 'draft', count: 151 },
  ],
};

function makeSearch(overrides: Partial<SearchConfig> = {}): SearchConfig {
  return {
    value: '',
    onChange: vi.fn(),
    onSubmit: vi.fn((e: FormEvent) => e.preventDefault()),
    placeholder: 'Search by title or URL...',
    ...overrides,
  };
}

function makeSort(overrides: Partial<SortConfig> = {}): SortConfig {
  return {
    key: 'sort_by',
    label: 'Sort',
    value: 'title',
    options: [
      { value: 'title', label: 'Title' },
      { value: 'word_count', label: 'Word count' },
    ],
    onChange: vi.fn(),
    ...overrides,
  };
}

function makeDateRange(
  overrides: Partial<DateRangeConfig> = {},
): DateRangeConfig {
  return {
    startKey: 'from',
    endKey: 'to',
    startValue: null,
    endValue: null,
    onChange: vi.fn(),
    ...overrides,
  };
}

function makeSlider(overrides: Partial<SliderConfig> = {}): SliderConfig {
  return {
    key: 'min_traffic',
    label: 'Min traffic',
    value: 0,
    min: 0,
    max: 100,
    onChange: vi.fn(),
    onCommit: vi.fn(),
    ...overrides,
  };
}

describe('TableFilterBar', () => {
  const user = userEvent.setup();

  it('renders the search input when a search config is provided', () => {
    render(
      <TableFilterBar
        search={makeSearch()}
        filters={[]}
        onChange={vi.fn()}
      />,
    );
    expect(
      screen.getByPlaceholderText('Search by title or URL...'),
    ).toBeInTheDocument();
  });

  it('omits the search input when no search config is provided', () => {
    render(<TableFilterBar filters={[]} onChange={vi.fn()} />);
    expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
  });

  it('renders one select trigger per filter with the correct accessible name', () => {
    render(
      <TableFilterBar
        filters={[TYPE_FILTER, STATUS_FILTER]}
        onChange={vi.fn()}
      />,
    );
    expect(screen.getByRole('combobox', { name: 'Filter by type' })).toBeInTheDocument();
    expect(
      screen.getByRole('combobox', { name: 'Filter by status' }),
    ).toBeInTheDocument();
  });

  it('calls onChange(key, value) when a filter option is selected', async () => {
    const onChange = vi.fn();
    render(
      <TableFilterBar filters={[TYPE_FILTER]} onChange={onChange} />,
    );
    await user.click(screen.getByRole('combobox', { name: 'Filter by type' }));
    await user.click(screen.getByRole('option', { name: /^post/ }));
    expect(onChange).toHaveBeenCalledWith('post_type', 'post');
  });

  it('calls onChange(key, null) when the "all" option is selected', async () => {
    const onChange = vi.fn();
    render(
      <TableFilterBar
        filters={[{ ...TYPE_FILTER, value: 'post' }]}
        onChange={onChange}
      />,
    );
    await user.click(screen.getByRole('combobox', { name: 'Filter by type' }));
    await user.click(screen.getByRole('option', { name: /All types/ }));
    expect(onChange).toHaveBeenCalledWith('post_type', null);
  });

  it('renders an active-filter chip labelled "{label}: {option label}" for each active filter', () => {
    render(
      <TableFilterBar
        filters={[
          { ...TYPE_FILTER, value: 'post' },
          { ...STATUS_FILTER, value: 'publish' },
        ]}
        onChange={vi.fn()}
      />,
    );
    expect(screen.getByText('Type: post')).toBeInTheDocument();
    expect(screen.getByText('Status: publish')).toBeInTheDocument();
  });

  it('falls back to the raw value when no matching option label exists', () => {
    render(
      <TableFilterBar
        filters={[{ ...TYPE_FILTER, value: 'unknown_value' }]}
        onChange={vi.fn()}
      />,
    );
    expect(screen.getByText('Type: unknown_value')).toBeInTheDocument();
  });

  it("clicking a filter chip's remove button clears only that filter", async () => {
    const onChange = vi.fn();
    render(
      <TableFilterBar
        filters={[
          { ...TYPE_FILTER, value: 'post' },
          { ...STATUS_FILTER, value: 'publish' },
        ]}
        onChange={onChange}
      />,
    );
    await user.click(
      screen.getByRole('button', { name: 'Remove type filter' }),
    );
    expect(onChange).toHaveBeenCalledTimes(1);
    expect(onChange).toHaveBeenCalledWith('post_type', null);
  });

  it('renders a search chip when search has a value and its remove button clears the search', async () => {
    const onChange = vi.fn();
    render(
      <TableFilterBar
        search={makeSearch({ value: 'react', onChange })}
        filters={[]}
        onChange={vi.fn()}
      />,
    );
    expect(screen.getByText('Search: "react"')).toBeInTheDocument();
    // Two clear-search controls exist (inline + chip); remove via the chip.
    await user.click(
      screen.getAllByRole('button', { name: 'Clear search' })[1],
    );
    expect(onChange).toHaveBeenCalledWith('');
  });

  it('renders an inline search clear button only when search has a value', async () => {
    const onChange = vi.fn();
    const { rerender } = render(
      <TableFilterBar
        search={makeSearch({ value: '', onChange })}
        filters={[]}
        onChange={vi.fn()}
      />,
    );
    expect(
      screen.queryByRole('button', { name: 'Clear search' }),
    ).not.toBeInTheDocument();

    rerender(
      <TableFilterBar
        search={makeSearch({ value: 'react', onChange })}
        filters={[]}
        onChange={vi.fn()}
      />,
    );
    const inlineClear = screen.getAllByRole('button', {
      name: 'Clear search',
    })[0];
    await user.click(inlineClear);
    expect(onChange).toHaveBeenCalledWith('');
  });

  it('shows the Clear (N) button with the correct count and clears everything', async () => {
    const onClearAll = vi.fn();
    const searchOnChange = vi.fn();
    render(
      <TableFilterBar
        search={makeSearch({ value: 'react', onChange: searchOnChange })}
        filters={[
          { ...TYPE_FILTER, value: 'post' },
          { ...STATUS_FILTER, value: 'publish' },
        ]}
        onChange={vi.fn()}
        onClearAll={onClearAll}
      />,
    );
    const clearBtn = screen.getByRole('button', { name: /^Clear \(3\)/ });
    expect(clearBtn).toBeInTheDocument();
    await user.click(clearBtn);
    expect(onClearAll).toHaveBeenCalledTimes(1);
    expect(searchOnChange).toHaveBeenCalledWith('');
  });

  it('falls back to per-filter clearing when no onClearAll is provided', async () => {
    const onChange = vi.fn();
    render(
      <TableFilterBar
        filters={[
          { ...TYPE_FILTER, value: 'post' },
          { ...STATUS_FILTER, value: 'publish' },
        ]}
        onChange={onChange}
      />,
    );
    await user.click(screen.getByRole('button', { name: /^Clear \(2\)/ }));
    expect(onChange).toHaveBeenCalledWith('post_type', null);
    expect(onChange).toHaveBeenCalledWith('post_status', null);
  });

  it('hides the clear button when nothing is active', () => {
    render(
      <TableFilterBar
        search={makeSearch()}
        filters={[TYPE_FILTER, STATUS_FILTER]}
        onChange={vi.fn()}
      />,
    );
    expect(
      screen.queryByRole('button', { name: /^Clear/ }),
    ).not.toBeInTheDocument();
  });

  it('renders "Showing N of M" when both resultCount and totalCount are provided', () => {
    render(
      <TableFilterBar
        filters={[]}
        onChange={vi.fn()}
        resultCount={9}
        totalCount={351}
      />,
    );
    expect(screen.getByText('Showing 9 of 351')).toBeInTheDocument();
  });

  it('renders no count summary when the count props are omitted', () => {
    render(<TableFilterBar filters={[]} onChange={vi.fn()} />);
    expect(screen.queryByText(/Showing/)).not.toBeInTheDocument();
  });

  it('disables the search input, selects, chip-remove buttons, and clear button when disabled', () => {
    render(
      <TableFilterBar
        search={makeSearch({ value: 'react' })}
        filters={[{ ...TYPE_FILTER, value: 'post' }]}
        onChange={vi.fn()}
        disabled
      />,
    );
    expect(screen.getByPlaceholderText('Search by title or URL...')).toBeDisabled();
    expect(screen.getByRole('combobox', { name: 'Filter by type' })).toBeDisabled();
    expect(screen.getByRole('button', { name: 'Remove type filter' })).toBeDisabled();
    // Inline + chip clear-search buttons.
    screen
      .getAllByRole('button', { name: 'Clear search' })
      .forEach((btn) => expect(btn).toBeDisabled());
    expect(screen.getByRole('button', { name: /^Clear \(/ })).toBeDisabled();
  });

  it('exposes the region with an accessible name and an associated search label', () => {
    render(
      <TableFilterBar
        search={makeSearch()}
        filters={[TYPE_FILTER]}
        onChange={vi.fn()}
      />,
    );
    expect(
      screen.getByRole('region', { name: 'Table filters' }),
    ).toBeInTheDocument();
    // The search input has an associated <label> (sr-only).
    expect(screen.getByLabelText('Search')).toBeInTheDocument();
  });

  it('forwards the inputRef to the underlying search input', () => {
    const ref = createRef<HTMLInputElement>();
    render(
      <TableFilterBar
        search={makeSearch({ inputRef: ref })}
        filters={[]}
        onChange={vi.fn()}
      />,
    );
    expect(ref.current).toBeInstanceOf(HTMLInputElement);
  });

  describe('sortConfig slot', () => {
    it('renders the sort dropdown and calls onChange when a sort option is selected', async () => {
      const onChange = vi.fn();
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sortConfig={makeSort({ onChange })}
        />,
      );
      await user.click(screen.getByRole('combobox', { name: 'Sort by' }));
      await user.click(screen.getByRole('option', { name: 'Word count' }));
      expect(onChange).toHaveBeenCalledWith('word_count');
    });

    it('calls onToggle when the direction toggle is clicked', async () => {
      const onToggle = vi.fn();
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sortConfig={makeSort({ direction: { value: 'desc', onToggle } })}
        />,
      );
      await user.click(
        screen.getByRole('button', { name: /Toggle sort direction/ }),
      );
      expect(onToggle).toHaveBeenCalledTimes(1);
    });

    it('renders a removable Sort chip only when the sort differs from its default, and removing resets to default', async () => {
      const onChange = vi.fn();
      const { rerender } = render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sortConfig={makeSort({ value: 'title', onChange })}
        />,
      );
      // First option ("title") is the implicit default → no chip.
      expect(screen.queryByText(/^Sort:/)).not.toBeInTheDocument();

      rerender(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sortConfig={makeSort({ value: 'word_count', onChange })}
        />,
      );
      expect(screen.getByText('Sort: Word count')).toBeInTheDocument();
      await user.click(screen.getByRole('button', { name: 'Remove sort' }));
      // Resets to the implicit default (first option).
      expect(onChange).toHaveBeenCalledWith('title');
    });

    it('uses an explicit defaultValue to decide chip activeness', () => {
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sortConfig={makeSort({ value: 'word_count', defaultValue: 'word_count' })}
        />,
      );
      // value === defaultValue → not active → no chip.
      expect(screen.queryByText(/^Sort:/)).not.toBeInTheDocument();
    });

    it('does not render an unclearable Sort chip when there is no resolvable default (empty options, no defaultValue)', () => {
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sortConfig={makeSort({ value: 'whatever', options: [] })}
        />,
      );
      // No default resolvable → sort not active → no chip and no Clear button.
      expect(screen.queryByText(/^Sort:/)).not.toBeInTheDocument();
      expect(
        screen.queryByRole('button', { name: /^Clear/ }),
      ).not.toBeInTheDocument();
    });
  });

  describe('dateRange slot', () => {
    it('renders two date inputs', () => {
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          dateRange={makeDateRange()}
        />,
      );
      expect(screen.getByLabelText('Dates from')).toBeInTheDocument();
      expect(screen.getByLabelText('Dates until')).toBeInTheDocument();
    });

    it('calls onChange with both keys when the start date changes', async () => {
      const onChange = vi.fn();
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          dateRange={makeDateRange({ onChange })}
        />,
      );
      const start = screen.getByLabelText('Dates from');
      fireEvent.change(start, { target: { value: '2026-01-01' } });
      expect(onChange).toHaveBeenCalledWith('from', '2026-01-01', 'to', null);
    });

    it('renders a single "<from> to <to>" chip when both dates are set', () => {
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          dateRange={makeDateRange({
            startValue: '2026-01-01',
            endValue: '2026-02-01',
          })}
        />,
      );
      expect(
        screen.getByText('Dates: 2026-01-01 to 2026-02-01'),
      ).toBeInTheDocument();
    });

    it('renders a "From <from>" chip when only the start is set', () => {
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          dateRange={makeDateRange({ startValue: '2026-01-01' })}
        />,
      );
      expect(screen.getByText('Dates: From 2026-01-01')).toBeInTheDocument();
    });

    it('renders an "Until <to>" chip when only the end is set, honouring a custom label', () => {
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          dateRange={makeDateRange({ endValue: '2026-02-01', label: 'Modified' })}
        />,
      );
      expect(screen.getByText('Modified: Until 2026-02-01')).toBeInTheDocument();
    });

    it('clears both dates when the date chip is removed', async () => {
      const onChange = vi.fn();
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          dateRange={makeDateRange({
            startValue: '2026-01-01',
            endValue: '2026-02-01',
            onChange,
          })}
        />,
      );
      await user.click(screen.getByRole('button', { name: 'Remove dates' }));
      expect(onChange).toHaveBeenCalledWith('from', null, 'to', null);
    });
  });

  describe('sliders slot', () => {
    it('renders a slider with a formatted live value label', () => {
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sliders={[
            makeSlider({ value: 20, format: (v) => `${v}+` }),
          ]}
        />,
      );
      // The visible field label + current value label.
      expect(screen.getByText('Min traffic')).toBeInTheDocument();
      expect(screen.getByText('20+')).toBeInTheDocument();
      // Radix renders role="slider" on the focusable thumb.
      const slider = screen.getByRole('slider');
      expect(slider).toHaveAttribute('aria-valuenow', '20');
    });

    it('calls onChange and onCommit when the slider is moved via keyboard', async () => {
      const onChange = vi.fn();
      const onCommit = vi.fn();
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sliders={[makeSlider({ value: 10, onChange, onCommit })]}
        />,
      );
      const slider = screen.getByRole('slider');
      slider.focus();
      await user.keyboard('{ArrowRight}');
      expect(onChange).toHaveBeenCalledWith('min_traffic', 11);
      expect(onCommit).toHaveBeenCalledWith('min_traffic', 11);
    });

    it('renders a chip for an above-min slider and resets to min on removal', async () => {
      const onChange = vi.fn();
      const onCommit = vi.fn();
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sliders={[
            makeSlider({ value: 40, onChange, onCommit, format: (v) => `${v}+` }),
          ]}
        />,
      );
      expect(screen.getByText('Min traffic: 40+')).toBeInTheDocument();
      await user.click(screen.getByRole('button', { name: 'Remove min traffic' }));
      expect(onChange).toHaveBeenCalledWith('min_traffic', 0);
      expect(onCommit).toHaveBeenCalledWith('min_traffic', 0);
    });

    it('renders no chip when the slider is at its minimum', () => {
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sliders={[makeSlider({ value: 0 })]}
        />,
      );
      expect(screen.queryByText(/^Min traffic:/)).not.toBeInTheDocument();
    });

    it('resets via onChange only (no throw) when onCommit is undefined and the chip is removed', async () => {
      const onChange = vi.fn();
      render(
        <TableFilterBar
          filters={[]}
          onChange={vi.fn()}
          sliders={[makeSlider({ value: 25, onChange, onCommit: undefined })]}
        />,
      );
      await user.click(screen.getByRole('button', { name: 'Remove min traffic' }));
      expect(onChange).toHaveBeenCalledWith('min_traffic', 0);
    });
  });

  describe('clear-all + active count across all slot types', () => {
    it('counts active search, dropdowns, sort, dates, and sliders together', () => {
      render(
        <TableFilterBar
          search={makeSearch({ value: 'react' })}
          filters={[{ ...TYPE_FILTER, value: 'post' }]}
          onChange={vi.fn()}
          onClearAll={vi.fn()}
          sortConfig={makeSort({ value: 'word_count' })}
          dateRange={makeDateRange({ startValue: '2026-01-01' })}
          sliders={[makeSlider({ value: 30 })]}
        />,
      );
      // search(1) + dropdown(1) + sort(1) + dates(1) + slider(1) = 5
      expect(
        screen.getByRole('button', { name: /^Clear \(5\)/ }),
      ).toBeInTheDocument();
    });

    it('clears search, dropdowns, sort, dates, and sliders when Clear all is clicked', async () => {
      const searchOnChange = vi.fn();
      const dropdownOnChange = vi.fn();
      const sortOnChange = vi.fn();
      const dateOnChange = vi.fn();
      const sliderOnChange = vi.fn();
      const sliderOnCommit = vi.fn();
      render(
        <TableFilterBar
          search={makeSearch({ value: 'react', onChange: searchOnChange })}
          filters={[{ ...TYPE_FILTER, value: 'post' }]}
          onChange={dropdownOnChange}
          sortConfig={makeSort({ value: 'word_count', onChange: sortOnChange })}
          dateRange={makeDateRange({
            startValue: '2026-01-01',
            endValue: '2026-02-01',
            onChange: dateOnChange,
          })}
          sliders={[
            makeSlider({
              value: 30,
              onChange: sliderOnChange,
              onCommit: sliderOnCommit,
            }),
          ]}
        />,
      );
      await user.click(screen.getByRole('button', { name: /^Clear \(5\)/ }));
      expect(searchOnChange).toHaveBeenCalledWith('');
      // No onClearAll → per-filter clearing.
      expect(dropdownOnChange).toHaveBeenCalledWith('post_type', null);
      expect(sortOnChange).toHaveBeenCalledWith('title');
      expect(dateOnChange).toHaveBeenCalledWith('from', null, 'to', null);
      expect(sliderOnChange).toHaveBeenCalledWith('min_traffic', 0);
      expect(sliderOnCommit).toHaveBeenCalledWith('min_traffic', 0);
    });
  });
});
