import { act, renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import { useFilters, countEffectiveFilters } from './useFilters';
import type { FeatureMeta } from '../types';

vi.mock('../lib/analytics', () => ({
  trackEvent: vi.fn(),
}));

describe('useFilters', () => {
  const features: FeatureMeta[] = [
    {
      name: 'price',
      type: 'numeric',
      min: 0,
      max: 100,
    },
  ];

  it('activates slider preview on pointer down without committing unchanged clicks', () => {
    const { result } = renderHook(() =>
      useFilters({
        initialFilters: { price: [0, 100] },
        features,
      })
    );

    act(() => {
      result.current.handleDragStart('price', [0, 100]);
    });

    expect(result.current.activeFeature).toBe('price');
    expect(result.current.viewSource).toBe('drag');
    expect(result.current.dragValue).toEqual([0, 100]);
    expect(result.current.filterRange).toEqual([0, 100]);

    act(() => {
      result.current.handleDragEnd();
    });

    expect(result.current.activeFeature).toBeNull();
    expect(result.current.dragValue).toBeNull();
    expect(result.current.filters.price).toEqual([0, 100]);

    act(() => {
      result.current.handleDragStart('price', [0, 100]);
      result.current.handleDragChange([10, 90]);
    });

    expect(result.current.activeFeature).toBe('price');
    expect(result.current.viewSource).toBe('drag');

    act(() => {
      result.current.handleDragEnd();
    });

    expect(result.current.activeFeature).toBeNull();
    expect(result.current.filters.price).toEqual([10, 90]);
  });

  it('caps the number of filters for demo users and reports when the limit is hit', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));
    const onFilterLimitReached = vi.fn();

    const { result } = renderHook(() =>
      useFilters({
        initialFilters: {},
        features: sixFeatures,
        filterLimit: 5,
        onFilterLimitReached,
      })
    );

    act(() => {
      ['a', 'b', 'c', 'd', 'e'].forEach((name) => result.current.handleAddFilter(name));
    });
    expect(Object.keys(result.current.filters)).toHaveLength(5);
    expect(onFilterLimitReached).not.toHaveBeenCalled();

    // The 6th distinct filter is blocked and reported.
    act(() => {
      result.current.handleAddFilter('f');
    });
    expect(Object.keys(result.current.filters)).toHaveLength(5);
    expect(result.current.filters.f).toBeUndefined();
    expect(onFilterLimitReached).toHaveBeenCalledTimes(1);

    // Re-adding an already-present feature is allowed (it replaces, no new key).
    act(() => {
      result.current.handleAddFilter('a');
    });
    expect(Object.keys(result.current.filters)).toHaveLength(5);
    expect(onFilterLimitReached).toHaveBeenCalledTimes(1);
  });

  it('caps bulk handleSetFilters (e.g. AI results) to the demo limit and reports it', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));
    const onFilterLimitReached = vi.fn();

    const { result } = renderHook(() =>
      useFilters({
        initialFilters: {},
        features: sixFeatures,
        filterLimit: 5,
        onFilterLimitReached,
      })
    );

    act(() => {
      result.current.handleSetFilters({
        a: [0, 1],
        b: [0, 1],
        c: [0, 1],
        d: [0, 1],
        e: [0, 1],
        f: [0, 1],
      });
    });

    expect(Object.keys(result.current.filters)).toHaveLength(5);
    expect(onFilterLimitReached).toHaveBeenCalledTimes(1);
  });

  it('clamps the initial filter set to the limit for non-paying users', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));

    // A bookmarked or shared URL may carry more filters than the cap allows; the
    // initial set must start clamped so the first map request never exceeds the
    // server-side cap (which would 400 and blank the map).
    const { result } = renderHook(() =>
      useFilters({
        initialFilters: { a: [0, 1], b: [0, 1], c: [0, 1], d: [0, 1], e: [0, 1] },
        features: sixFeatures,
        filterLimit: 3,
      })
    );

    expect(Object.keys(result.current.filters)).toHaveLength(3);
  });

  it('does not clamp the initial filter set when filterLimit is null (licensed users)', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));

    const { result } = renderHook(() =>
      useFilters({
        initialFilters: { a: [0, 1], b: [0, 1], c: [0, 1], d: [0, 1], e: [0, 1] },
        features: sixFeatures,
        filterLimit: null,
      })
    );

    expect(Object.keys(result.current.filters)).toHaveLength(5);
  });

  it('counts reserved slots (e.g. travel-time filters) toward the cap', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));
    const onFilterLimitReached = vi.fn();

    // Limit 3 with 1 slot already reserved (e.g. a travel-time filter) leaves room
    // for only 2 feature filters.
    const { result } = renderHook(() =>
      useFilters({
        initialFilters: {},
        features: sixFeatures,
        filterLimit: 3,
        reservedFilterSlots: 1,
        onFilterLimitReached,
      })
    );

    act(() => {
      ['a', 'b'].forEach((name) => result.current.handleAddFilter(name));
    });
    expect(Object.keys(result.current.filters)).toHaveLength(2);
    expect(onFilterLimitReached).not.toHaveBeenCalled();

    act(() => {
      result.current.handleAddFilter('c');
    });
    expect(Object.keys(result.current.filters)).toHaveLength(2);
    expect(onFilterLimitReached).toHaveBeenCalledTimes(1);
  });

  it('clamps the initial filter set accounting for reserved slots', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));

    // 4 initial feature filters, limit 3, 1 reserved slot → keep only 2 features.
    const { result } = renderHook(() =>
      useFilters({
        initialFilters: { a: [0, 1], b: [0, 1], c: [0, 1], d: [0, 1] },
        features: sixFeatures,
        filterLimit: 3,
        reservedFilterSlots: 1,
      })
    );

    expect(Object.keys(result.current.filters)).toHaveLength(2);
  });

  it('blocks adding new filters when lockAdds is set (shared link), but allows replacing existing ones', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));
    const onFilterLimitReached = vi.fn();

    // A non-paying user on a shared link: still under the cap (5), but adds are
    // locked. They can adjust the shared filter ('a') but not add a new one.
    const { result } = renderHook(() =>
      useFilters({
        initialFilters: { a: [0, 100] },
        features: sixFeatures,
        filterLimit: 5,
        lockAdds: true,
        onFilterLimitReached,
      })
    );

    // Adding a brand-new filter is blocked and reported, even though count < cap.
    act(() => {
      result.current.handleAddFilter('b');
    });
    expect(Object.keys(result.current.filters)).toHaveLength(1);
    expect(result.current.filters.b).toBeUndefined();
    expect(onFilterLimitReached).toHaveBeenCalledTimes(1);

    // Re-applying the existing filter replaces it (no new key) and is allowed.
    act(() => {
      result.current.handleAddFilter('a');
    });
    expect(Object.keys(result.current.filters)).toHaveLength(1);
    expect(onFilterLimitReached).toHaveBeenCalledTimes(1);
  });

  it('strips new keys from a bulk set (AI filters) when lockAdds is set, keeping existing ones', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));
    const onFilterLimitReached = vi.fn();

    // A non-paying user on a shared link runs an AI query, which goes through the
    // bulk handleSetFilters path. lockAdds must apply here too (not just per-add),
    // otherwise the AI box becomes a way to build a fresh filter set on a shared link.
    const { result } = renderHook(() =>
      useFilters({
        initialFilters: { a: [0, 100] },
        features: sixFeatures,
        filterLimit: 5,
        lockAdds: true,
        onFilterLimitReached,
      })
    );

    // A bulk set that adjusts the existing key 'a' alongside new keys ('b','c') keeps
    // only 'a' (its new value), drops the new keys, and reports the block.
    act(() => {
      result.current.handleSetFilters({ a: [10, 90], b: [0, 50], c: [0, 50] });
    });
    expect(Object.keys(result.current.filters)).toEqual(['a']);
    expect(result.current.filters.a).toEqual([10, 90]);
    expect(onFilterLimitReached).toHaveBeenCalledTimes(1);

    // An all-new bulk set (no existing key survives) is restricted to {} and reported.
    act(() => {
      result.current.handleSetFilters({ d: [0, 50], e: [0, 50] });
    });
    expect(Object.keys(result.current.filters)).toHaveLength(0);
    expect(onFilterLimitReached).toHaveBeenCalledTimes(2);
  });

  it('does not cap filters when filterLimit is null (licensed users)', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));

    const { result } = renderHook(() =>
      useFilters({
        initialFilters: {},
        features: sixFeatures,
        filterLimit: null,
      })
    );

    act(() => {
      ['a', 'b', 'c', 'd', 'e', 'f'].forEach((name) => result.current.handleAddFilter(name));
    });
    expect(Object.keys(result.current.filters)).toHaveLength(6);
  });

  it('uses the provided initial range for drag-only feature keys', () => {
    const { result } = renderHook(() =>
      useFilters({
        initialFilters: {},
        features,
      })
    );

    act(() => {
      result.current.handleDragStart('tt_car_station', [15, 45]);
    });

    expect(result.current.activeFeature).toBe('tt_car_station');
    expect(result.current.dragValue).toEqual([15, 45]);
    expect(result.current.filterRange).toEqual([15, 45]);

    act(() => {
      result.current.handleDragEnd();
    });

    expect(result.current.activeFeature).toBeNull();
    expect(result.current.dragValue).toBeNull();
    expect(result.current.filters).toEqual({});
  });

  it('drops unknown initial keys before applying the cap, not after', () => {
    const knownFeatures: FeatureMeta[] = ['a', 'b', 'c'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));

    // 3 valid keys + 2 stale/unknown keys, cap 3. The unknowns must be dropped first so
    // the cap is spent on the 3 real filters (all kept), not wasted on stale keys.
    const { result } = renderHook(() =>
      useFilters({
        initialFilters: {
          a: [0, 100],
          stale1: [0, 100],
          b: [0, 100],
          stale2: [0, 100],
          c: [0, 100],
        },
        features: knownFeatures,
        filterLimit: 3,
      })
    );

    expect(Object.keys(result.current.filters).sort()).toEqual(['a', 'b', 'c']);
  });

  it('recovers genuine filters lost to a cold-load slice once features arrive', () => {
    const knownFeatures: FeatureMeta[] = ['a', 'b', 'c'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));
    // Cold load: a link with stale keys ordered before genuine ones, over budget. At
    // mount features=[] so the init-clamp can't drop unknowns and the blind slice keeps
    // the first 3 keys (2 stale + 1 genuine). Once features load, the genuine filters
    // must be recovered, not left as just the one that happened to survive the slice.
    const initialFilters = {
      stale1: [0, 100] as [number, number],
      stale2: [0, 100] as [number, number],
      a: [0, 100] as [number, number],
      b: [0, 100] as [number, number],
      c: [0, 100] as [number, number],
    };
    const { result, rerender } = renderHook(
      (props: { features: FeatureMeta[] }) =>
        useFilters({ initialFilters, features: props.features, filterLimit: 3 }),
      { initialProps: { features: [] as FeatureMeta[] } }
    );

    // Cold mount: blind slice keeps the first 3 keys, including stale ones.
    expect(Object.keys(result.current.filters)).toEqual(['stale1', 'stale2', 'a']);

    // Features arrive: re-derive from source → the 3 genuine filters, stale dropped.
    rerender({ features: knownFeatures });
    expect(Object.keys(result.current.filters).sort()).toEqual(['a', 'b', 'c']);
  });

  it('trims filters down to the new cap when the cap tightens (logout / session loss)', () => {
    const sixFeatures: FeatureMeta[] = ['a', 'b', 'c', 'd', 'e', 'f'].map((name) => ({
      name,
      type: 'numeric',
      min: 0,
      max: 100,
    }));
    const initialFilters = {
      a: [0, 100] as [number, number],
      b: [0, 100] as [number, number],
      c: [0, 100] as [number, number],
      d: [0, 100] as [number, number],
      e: [0, 100] as [number, number],
    };
    const { result, rerender } = renderHook(
      (props: { filterLimit: number }) =>
        useFilters({ initialFilters, features: sixFeatures, filterLimit: props.filterLimit }),
      { initialProps: { filterLimit: 5 } }
    );

    // Registered: 5 filters fit the cap.
    expect(Object.keys(result.current.filters)).toHaveLength(5);

    // Cap drops to 3 (logout, or a server-invalidated session): filters trim to 3 so the
    // next data request doesn't exceed the server cap and blank the map.
    rerender({ filterLimit: 3 });
    expect(Object.keys(result.current.filters)).toHaveLength(3);
  });
});

describe('countEffectiveFilters', () => {
  const features: FeatureMeta[] = ['a', 'b', 'c'].map((name) => ({
    name,
    type: 'numeric',
    min: 0,
    max: 100,
  }));

  it('counts only known features, ignoring stale/unknown keys', () => {
    // The over-cap shared-link popup keys off this: stale keys must not inflate the count
    // and trigger a false "your search was trimmed" alarm.
    expect(countEffectiveFilters({ a: [0, 1], b: [0, 1], gone: [0, 1] }, features)).toBe(2);
    expect(countEffectiveFilters({ a: [0, 1], b: [0, 1], c: [0, 1] }, features)).toBe(3);
  });

  it('counts everything when the feature list is empty (still loading)', () => {
    expect(countEffectiveFilters({ a: [0, 1], b: [0, 1] }, [])).toBe(2);
  });
});
