import { describe, expect, it } from 'vitest';

import type { FeatureMeta } from '../types';
import { apiUrl, assertOk, buildFilterString, isAbortError, paramsWithLanguage } from './api';
import { createSchoolFilterKey } from './school-filter';
import { createSpecificCrimeFilterKey } from './crime-filter';
import { createElectionVoteShareFilterKey } from './election-filter';
import { createEthnicityFilterKey } from './ethnicity-filter';
import {
  POI_COUNT_2KM_FILTER_NAME,
  TRANSPORT_DISTANCE_FILTER_NAME,
  createPoiDistanceFilterKey,
  createPoiFilterKey,
} from './poi-distance-filter';

describe('api utilities', () => {
  it('builds API URLs from endpoint names, paths, and params', () => {
    expect(apiUrl('features')).toBe('/api/features');
    expect(apiUrl('/custom/path')).toBe('/custom/path');
    expect(apiUrl('hexagons', new URLSearchParams({ bounds: '1,2,3,4' }))).toBe(
      '/api/hexagons?bounds=1%2C2%2C3%2C4'
    );
  });

  it('throws helpful errors for non-OK responses', () => {
    expect(() => assertOk(new Response(null, { status: 204 }), 'empty')).not.toThrow();
    expect(() =>
      assertOk(new Response(null, { status: 404, statusText: 'Not Found' }), 'lookup')
    ).toThrow('lookup: HTTP 404 Not Found');
  });

  it('recognizes AbortError instances', () => {
    const abort = new Error('Aborted');
    abort.name = 'AbortError';
    const regular = new Error('nope');

    expect(isAbortError(abort)).toBe(true);
    expect(isAbortError(regular)).toBe(false);
  });

  it('adds supported language parameters without overriding explicit languages', () => {
    expect(paramsWithLanguage('lat=51.5&lon=-0.1', 'fr-FR')).toBe('lat=51.5&lon=-0.1&lang=fr');
    expect(paramsWithLanguage('lat=51.5&lang=de', 'fr')).toBe('lat=51.5&lang=de');
  });

  it('serializes numeric, absolute, and enum filters for backend routes', () => {
    const features: FeatureMeta[] = [
      { name: 'Last known price', type: 'numeric', min: 0, max: 1_000_000 },
      {
        name: 'Estimated current price',
        type: 'numeric',
        absolute: true,
        histogram: { min: 0, max: 2_000_000, p1: 0, p99: 2_000_000, counts: [1] },
      },
      { name: 'Property type', type: 'enum', values: ['Flat', 'House'] },
    ];

    expect(
      buildFilterString(
        {
          'Last known price': [100_000, 500_000],
          'Estimated current price': [0, 2_000_000],
          'Property type': ['Flat', 'House'],
        },
        features
      )
    ).toBe(
      'Last known price:100000:500000;;Estimated current price:0:inf;;Property type:Flat|House'
    );

    expect(
      buildFilterString(
        {
          'Last known price': [100_000, 500_000],
          'Property type': ['Flat'],
        },
        features,
        'Last known price'
      )
    ).toBe('Property type:Flat');
  });

  it('deduplicates repeated synthetic school filters before backend routes', () => {
    const features: FeatureMeta[] = [
      { name: 'Good+ primary school catchments', type: 'numeric', min: 0, max: 10 },
    ];

    expect(
      buildFilterString(
        {
          [createSchoolFilterKey('primary', 'good', 1)]: [1, 10],
          [createSchoolFilterKey('primary', 'good', 2)]: [2, 8],
        },
        features
      )
    ).toBe('Good+ primary school catchments:2:8');
  });

  it('serializes specific crime filters using their selected backend crime feature', () => {
    const features: FeatureMeta[] = [
      { name: 'Burglary (avg/yr)', type: 'numeric', min: 0, max: 20 },
      { name: 'Vehicle crime (avg/yr)', type: 'numeric', min: 0, max: 30 },
    ];

    expect(
      buildFilterString(
        {
          [createSpecificCrimeFilterKey('Burglary (avg/yr)', 1)]: [0, 5],
          [createSpecificCrimeFilterKey('Vehicle crime (avg/yr)', 2)]: [1, 10],
        },
        features
      )
    ).toBe('Burglary (avg/yr):0:5;;Vehicle crime (avg/yr):1:10');
  });

  it('serializes election vote-share filters using their selected backend party feature', () => {
    const features: FeatureMeta[] = [
      { name: '% Labour', type: 'numeric', min: 0, max: 100 },
      { name: '% Conservative', type: 'numeric', min: 0, max: 100 },
    ];

    expect(
      buildFilterString(
        {
          [createElectionVoteShareFilterKey('% Labour', 1)]: [30, 60],
          [createElectionVoteShareFilterKey('% Conservative', 2)]: [10, 40],
        },
        features
      )
    ).toBe('% Labour:30:60;;% Conservative:10:40');
  });

  it('deduplicates repeated ethnicity filters to the strictest backend range', () => {
    const features: FeatureMeta[] = [{ name: '% White', type: 'numeric', min: 0, max: 100 }];

    expect(
      buildFilterString(
        {
          [createEthnicityFilterKey('% White', 1)]: [10, 90],
          [createEthnicityFilterKey('% White', 2)]: [20, 80],
        },
        features
      )
    ).toBe('% White:20:80');
  });

  it('serializes amenity distance filters using their selected backend feature', () => {
    const features: FeatureMeta[] = [
      { name: 'Distance to nearest amenity (Park) (km)', type: 'numeric', min: 0, max: 2 },
      { name: 'Distance to nearest amenity (Café) (km)', type: 'numeric', min: 0, max: 5 },
    ];

    expect(
      buildFilterString(
        {
          [createPoiDistanceFilterKey('Distance to nearest amenity (Park) (km)', 1)]: [0, 0.5],
          [createPoiDistanceFilterKey('Distance to nearest amenity (Café) (km)', 2)]: [0, 1],
        },
        features
      )
    ).toBe(
      'Distance to nearest amenity (Park) (km):0:0.5;;Distance to nearest amenity (Café) (km):0:1'
    );
  });

  it('serializes amenity count filters using their selected backend feature', () => {
    const features: FeatureMeta[] = [
      { name: 'Number of amenities (Cafe) within 2km', type: 'numeric', min: 0, max: 20 },
    ];

    expect(
      buildFilterString(
        {
          [createPoiFilterKey(
            POI_COUNT_2KM_FILTER_NAME,
            'Number of amenities (Cafe) within 2km',
            1
          )]: [2, 10],
        },
        features
      )
    ).toBe('Number of amenities (Cafe) within 2km:2:10');
  });

  it('serializes transport distance filters using their selected backend feature', () => {
    const features: FeatureMeta[] = [
      { name: 'Distance to nearest amenity (Bus stop) (km)', type: 'numeric', min: 0, max: 2 },
    ];

    expect(
      buildFilterString(
        {
          [createPoiFilterKey(
            TRANSPORT_DISTANCE_FILTER_NAME,
            'Distance to nearest amenity (Bus stop) (km)',
            1
          )]: [0, 0.4],
        },
        features
      )
    ).toBe('Distance to nearest amenity (Bus stop) (km):0:0.4');
  });
});
