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

import type { FeatureMeta } from '../types';
import { parseUrlState, stateToParams } from './url-state';
import { INITIAL_VIEW_STATE } from './consts';
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('url-state', () => {
  beforeEach(() => {
    window.history.replaceState({}, '', '/');
  });

  it('parses view, filters, POIs, tab, postcode, and travel-time params', () => {
    window.history.replaceState(
      {},
      '',
      '/?lat=51.5074&lon=-0.1278&zoom=12.5&filter=Last%20known%20price:100000:500000&filter=Property%20type:Flat|House&poi=supermarket&tab=properties&pc=SW1A%201AA&tt=transit:kings-cross:Kings%20Cross:b:0:30'
    );

    const state = parseUrlState();

    expect(state.viewState).toEqual({
      latitude: 51.5074,
      longitude: -0.1278,
      zoom: 12.5,
      pitch: 0,
    });
    expect(state.filters).toEqual({
      'Last known price': [100000, 500000],
      'Property type': ['Flat', 'House'],
    });
    expect(state.poiCategories).toEqual(new Set(['supermarket']));
    expect(state.tab).toBe('properties');
    expect(state.postcode).toBe('SW1A 1AA');
    expect(state.travelTime?.entries).toEqual([
      {
        mode: 'transit',
        slug: 'kings-cross',
        label: 'Kings Cross',
        timeRange: [0, 30],
        useBest: true,
        noChange: false,
        oneChange: false,
        noBuses: false,
      },
    ]);
  });

  it('leaves POIs unselected when URL params are omitted', () => {
    const state = parseUrlState();

    expect(state.viewState).toEqual(INITIAL_VIEW_STATE);
    expect(state.filters).toEqual({});
    expect(state.poiCategories).toEqual(new Set());
    expect(state.tab).toBe('area');
  });

  it('serializes map state and active filters into stable URL params', () => {
    const features: FeatureMeta[] = [
      { name: 'Last known price', type: 'numeric' },
      { name: 'Property type', type: 'enum', values: ['Flat', 'House'] },
    ];

    const params = stateToParams(
      { latitude: 51.50742, longitude: -0.12781, zoom: 12.47 },
      {
        'Last known price': [100000, 500000],
        'Property type': ['Flat', 'House'],
      },
      features,
      new Set(['supermarket']),
      'properties',
      [
        {
          mode: 'bicycle',
          slug: 'bank',
          label: 'Bank',
          useBest: false,
          timeRange: [5, 25],
        },
      ]
    );

    expect(params.get('lat')).toBe('51.5074');
    expect(params.get('lon')).toBe('-0.1278');
    expect(params.get('zoom')).toBe('12.5');
    expect(params.getAll('filter')).toEqual([
      'Last known price:100000:500000',
      'Property type:Flat|House',
    ]);
    expect(params.getAll('poi')).toEqual(['supermarket']);
    expect(params.get('tab')).toBe('properties');
    expect(params.getAll('tt')).toEqual(['bicycle:bank:Bank:5:25']);
  });

  it('deduplicates travel-time URL params with the tightest range', () => {
    window.history.replaceState(
      {},
      '',
      '/?tt=transit:bank-tube-station:Bank:0:60&tt=transit:bank-tube-station:Bank:10:45'
    );

    const state = parseUrlState();

    expect(state.travelTime?.entries).toEqual([
      {
        mode: 'transit',
        slug: 'bank-tube-station',
        label: 'Bank',
        timeRange: [10, 45],
        useBest: false,
        noChange: false,
        oneChange: false,
        noBuses: false,
      },
    ]);

    const params = stateToParams(null, {}, [], new Set(), 'area', [
      {
        mode: 'transit',
        slug: 'bank-tube-station',
        label: 'Bank',
        useBest: false,
        timeRange: [0, 60],
      },
      {
        mode: 'transit',
        slug: 'bank-tube-station',
        label: 'Bank',
        useBest: false,
        timeRange: [10, 45],
      },
    ]);

    expect(params.getAll('tt')).toEqual(['transit:bank-tube-station:Bank:10:45']);
  });

  it('round-trips an explicitly empty POI selection', () => {
    const params = stateToParams(null, {}, [], new Set(), 'area');

    expect(params.getAll('poi')).toEqual(['__none']);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.poiCategories).toEqual(new Set());
  });

  it('serializes a selected postcode for saved dashboard URLs', () => {
    const params = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      'SW1A 1AA'
    );

    expect(params.get('pc')).toBe('SW1A 1AA');

    window.history.replaceState({}, '', `/?${params.toString()}`);
    expect(parseUrlState().postcode).toBe('SW1A 1AA');
  });

  it('round-trips overlay selections', () => {
    const params = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      new Set(['noise', 'crime-hotspots'])
    );

    expect(params.getAll('overlay')).toEqual(['noise', 'crime-hotspots']);

    window.history.replaceState({}, '', `/?${params.toString()}&overlay=unknown`);
    const state = parseUrlState();

    expect(state.overlays).toEqual(new Set(['noise', 'crime-hotspots']));
  });

  it('enables crime + trees overlays by default on a fresh visit', () => {
    const state = parseUrlState();

    expect(state.overlays).toEqual(new Set(['crime-hotspots', 'trees-outside-woodlands']));
  });

  it('round-trips an explicit "all overlays off" via the __none sentinel', () => {
    const params = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      new Set()
    );

    expect(params.getAll('overlay')).toEqual(['__none']);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.overlays).toEqual(new Set());
  });

  it('round-trips satellite basemap selection', () => {
    const params = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      undefined,
      'satellite'
    );

    expect(params.get('basemap')).toBe('satellite');

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.basemap).toBe('satellite');

    window.history.replaceState({}, '', '/?basemap=unknown');
    expect(parseUrlState().basemap).toBe('standard');
  });

  it('round-trips non-default colour opacity', () => {
    const defaultParams = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      1
    );
    expect(defaultParams.has('colorOpacity')).toBe(false);

    const params = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      0.45
    );

    expect(params.get('colorOpacity')).toBe('45');

    window.history.replaceState({}, '', `/?${params.toString()}`);
    expect(parseUrlState().colorOpacity).toBe(0.45);

    window.history.replaceState({}, '', '/?colorOpacity=not-a-number');
    expect(parseUrlState().colorOpacity).toBe(1);

    window.history.replaceState({}, '', '/?colorOpacity=0');
    expect(parseUrlState().colorOpacity).toBe(0.1);
  });

  it('round-trips a crime-type subset when the crime overlay is active', () => {
    const params = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      new Set(['crime-hotspots']),
      undefined,
      new Set(['Burglary', 'Robbery'])
    );

    expect(params.getAll('crimeType')).toEqual(['Burglary', 'Robbery']);

    window.history.replaceState({}, '', `/?${params.toString()}&crimeType=NotAType`);
    expect(parseUrlState().crimeTypes).toEqual(new Set(['Burglary', 'Robbery']));
  });

  it('omits crime types when all are selected or the crime overlay is off', () => {
    const allSelected = new Set([
      'Violence and sexual offences',
      'Anti-social behaviour',
      'Criminal damage and arson',
      'Public order',
      'Shoplifting',
      'Vehicle crime',
      'Burglary',
      'Other theft',
      'Theft from the person',
      'Bicycle theft',
      'Drugs',
      'Robbery',
      'Possession of weapons',
      'Other crime',
    ]);
    const allParams = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      new Set(['crime-hotspots']),
      undefined,
      allSelected
    );
    expect(allParams.getAll('crimeType')).toEqual([]);

    // Subset selected but the crime overlay is not active → nothing emitted.
    const overlayOffParams = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      new Set(['noise']),
      undefined,
      new Set(['Burglary'])
    );
    expect(overlayOffParams.getAll('crimeType')).toEqual([]);

    expect(parseUrlState().crimeTypes).toBeUndefined();
  });

  it('round-trips a fully-deselected crime-type state via sentinel', () => {
    const params = stateToParams(
      null,
      {},
      [],
      new Set(),
      'area',
      undefined,
      undefined,
      new Set(['crime-hotspots']),
      undefined,
      new Set()
    );

    expect(params.getAll('crimeType')).toEqual(['__none']);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    expect(parseUrlState().crimeTypes).toEqual(new Set());
  });

  it('round-trips repeated school filters with dedicated URL params', () => {
    const schoolOne = createSchoolFilterKey('primary', 'good', 1);
    const schoolTwo = createSchoolFilterKey('secondary', 'outstanding', 2);

    const params = stateToParams(
      null,
      {
        [schoolOne]: [1, 10],
        [schoolTwo]: [2, 15],
      },
      [],
      new Set(),
      'area'
    );

    expect(params.getAll('school')).toEqual(['primary:good:1:10', 'secondary:outstanding:2:15']);
    expect(params.getAll('filter')).toEqual([]);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.filters).toEqual({
      [createSchoolFilterKey('primary', 'good', 0)]: [1, 10],
      [createSchoolFilterKey('secondary', 'outstanding', 1)]: [2, 15],
    });
  });

  it('parses legacy school URL params that still carry a distance segment', () => {
    window.history.replaceState({}, '', '/?school=primary%3Agood%3A2%3A1%3A10');
    expect(parseUrlState().filters).toEqual({
      [createSchoolFilterKey('primary', 'good', 0)]: [1, 10],
    });
  });

  it('round-trips repeated specific crime filters with dedicated URL params', () => {
    const burglary = createSpecificCrimeFilterKey('Burglary (avg/yr)', 1);
    const vehicleCrime = createSpecificCrimeFilterKey('Vehicle crime (avg/yr)', 2);

    const params = stateToParams(
      null,
      {
        [burglary]: [0, 5],
        [vehicleCrime]: [1, 10],
      },
      [],
      new Set(),
      'area'
    );

    expect(params.getAll('crime')).toEqual([
      'Burglary (avg/yr):0:5',
      'Vehicle crime (avg/yr):1:10',
    ]);
    expect(params.getAll('filter')).toEqual([]);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.filters).toEqual({
      [createSpecificCrimeFilterKey('Burglary (avg/yr)', 0)]: [0, 5],
      [createSpecificCrimeFilterKey('Vehicle crime (avg/yr)', 1)]: [1, 10],
    });
  });

  it('round-trips repeated election vote-share filters with dedicated URL params', () => {
    const labour = createElectionVoteShareFilterKey('% Labour', 1);
    const conservative = createElectionVoteShareFilterKey('% Conservative', 2);

    const params = stateToParams(
      null,
      {
        [labour]: [30, 55],
        [conservative]: [10, 35],
      },
      [],
      new Set(),
      'area'
    );

    expect(params.getAll('voteShare')).toEqual(['% Labour:30:55', '% Conservative:10:35']);
    expect(params.getAll('filter')).toEqual([]);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.filters).toEqual({
      [createElectionVoteShareFilterKey('% Labour', 0)]: [30, 55],
      [createElectionVoteShareFilterKey('% Conservative', 1)]: [10, 35],
    });
  });

  it('round-trips repeated ethnicity filters with dedicated URL params', () => {
    const white = createEthnicityFilterKey('% White', 3);
    const southAsian = createEthnicityFilterKey('% South Asian', 4);

    const params = stateToParams(
      null,
      {
        [white]: [10, 80],
        [southAsian]: [5, 35],
      },
      [],
      new Set(),
      'area'
    );

    expect(params.getAll('ethnicity')).toEqual(['% White:10:80', '% South Asian:5:35']);
    expect(params.getAll('filter')).toEqual([]);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.filters).toEqual({
      [createEthnicityFilterKey('% White', 0)]: [10, 80],
      [createEthnicityFilterKey('% South Asian', 1)]: [5, 35],
    });
  });

  it('round-trips repeated amenity distance filters with dedicated URL params', () => {
    const park = createPoiDistanceFilterKey('Distance to nearest amenity (Park) (km)', 3);
    const cafe = createPoiDistanceFilterKey('Distance to nearest amenity (Café) (km)', 4);

    const params = stateToParams(
      null,
      {
        [park]: [0, 0.4],
        [cafe]: [0, 1.5],
      },
      [],
      new Set(),
      'area'
    );

    expect(params.getAll('amenityDistance')).toEqual([
      'Distance%20to%20nearest%20amenity%20(Park)%20(km):0:0.4',
      'Distance%20to%20nearest%20amenity%20(Caf%C3%A9)%20(km):0:1.5',
    ]);
    expect(params.getAll('filter')).toEqual([]);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.filters).toEqual({
      [createPoiDistanceFilterKey('Distance to nearest amenity (Park) (km)', 0)]: [0, 0.4],
      [createPoiDistanceFilterKey('Distance to nearest amenity (Café) (km)', 1)]: [0, 1.5],
    });
  });

  it('round-trips transport distance filters with dedicated URL params', () => {
    const busStop = createPoiFilterKey(
      TRANSPORT_DISTANCE_FILTER_NAME,
      'Distance to nearest amenity (Bus stop) (km)',
      3
    );

    const params = stateToParams(
      null,
      {
        [busStop]: [0, 0.3],
      },
      [],
      new Set(),
      'area'
    );

    expect(params.getAll('transportDistance')).toEqual([
      'Distance%20to%20nearest%20amenity%20(Bus%20stop)%20(km):0:0.3',
    ]);
    expect(params.getAll('amenityDistance')).toEqual([]);
    expect(params.getAll('filter')).toEqual([]);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.filters).toEqual({
      [createPoiFilterKey(
        TRANSPORT_DISTANCE_FILTER_NAME,
        'Distance to nearest amenity (Bus stop) (km)',
        0
      )]: [0, 0.3],
    });
  });

  it('round-trips amenity count filters with dedicated URL params', () => {
    const cafes = createPoiFilterKey(
      POI_COUNT_2KM_FILTER_NAME,
      'Number of amenities (Cafe) within 2km',
      3
    );

    const params = stateToParams(
      null,
      {
        [cafes]: [2, 8],
      },
      [],
      new Set(),
      'area'
    );

    expect(params.getAll('amenityCount2km')).toEqual([
      'Number%20of%20amenities%20(Cafe)%20within%202km:2:8',
    ]);
    expect(params.getAll('filter')).toEqual([]);

    window.history.replaceState({}, '', `/?${params.toString()}`);
    const state = parseUrlState();

    expect(state.filters).toEqual({
      [createPoiFilterKey(POI_COUNT_2KM_FILTER_NAME, 'Number of amenities (Cafe) within 2km', 0)]: [
        2, 8,
      ],
    });
  });

  it('omits the default area tab', () => {
    const params = stateToParams(null, {}, [], new Set(), 'area');

    expect(params.has('tab')).toBe(false);
  });
});
