import { describe, it, expect, vi, afterEach } from 'vitest';
import {
  readUiUrlState,
  buildUiUrlStateSearch,
  paramsEqualExcept,
  COLUMN_LABELS,
  COLUMN_LIST,
  type UiUrlState,
} from './urlState';

const DEFAULT_STATE: UiUrlState = {
  page: 'tasks',
  repo: '',
  status: 'all',
  search: '',
  sort: 'updated',
  scope: 'mine',
  detailTab: 'overview',
  mobileTab: 'chat',
  taskId: null,
};

function stubLocation(search: string) {
  vi.stubGlobal('window', { location: { search } });
}

afterEach(() => vi.unstubAllGlobals());

describe('readUiUrlState', () => {
  it('returns defaults when window is undefined (SSR/node)', () => {
    // node environment: no window global by default
    expect(readUiUrlState()).toEqual(DEFAULT_STATE);
  });

  it('returns defaults for an empty query string', () => {
    stubLocation('');
    const state = readUiUrlState();
    expect(state).toEqual({
      ...DEFAULT_STATE,
      section: undefined,
      piece: undefined,
    });
  });

  it('parses every valid parameter', () => {
    stubLocation(
      '?page=settings&repo=local%2Ftask-1&status=running&q=hello&sort=title' +
      '&tab=files&mobileTab=activity&task=42&section=auth&piece=chat' +
      '&pieceSource=user-custom&dashboardWidget=gpu&help=intro',
    );
    const state = readUiUrlState();
    expect(state.page).toBe('settings');
    expect(state.repo).toBe('local/task-1');
    expect(state.status).toBe('running');
    expect(state.search).toBe('hello');
    expect(state.sort).toBe('title');
    expect(state.detailTab).toBe('files');
    expect(state.mobileTab).toBe('activity');
    expect(state.taskId).toBe(42);
    expect(state.section).toBe('auth');
    expect(state.piece).toBe('chat');
    expect(state.pieceSource).toBe('user-custom');
    expect(state.dashboardWidget).toBe('gpu');
    expect(state.help).toBe('intro');
  });

  it('falls back to defaults on invalid enum values', () => {
    stubLocation('?page=bogus&status=bogus&sort=bogus&tab=bogus&mobileTab=bogus&section=bogus&pieceSource=bogus');
    const state = readUiUrlState();
    expect(state.page).toBe('tasks');
    expect(state.status).toBe('all');
    expect(state.sort).toBe('updated');
    expect(state.detailTab).toBe('overview');
    expect(state.mobileTab).toBe('chat');
    expect(state.section).toBeUndefined();
    expect(state.pieceSource).toBeUndefined();
  });

  it('rejects non-positive and non-numeric task ids', () => {
    for (const task of ['0', '-5', 'abc', '']) {
      stubLocation(`?task=${task}`);
      expect(readUiUrlState().taskId).toBeNull();
    }
    stubLocation('?task=7');
    expect(readUiUrlState().taskId).toBe(7);
  });

  it('still parses legacy settings section ids (soft bookmark landing)', () => {
    for (const legacy of ['gateway-keys', 'provider', 'workspace', 'browser-sessions']) {
      stubLocation(`?section=${legacy}`);
      expect(readUiUrlState().section).toBe(legacy);
    }
  });

  it('treats an empty piece param as undefined', () => {
    stubLocation('?piece=');
    expect(readUiUrlState().piece).toBeUndefined();
  });

  it('omits optional spread keys entirely when their params are absent', () => {
    stubLocation('?page=tasks');
    const state = readUiUrlState();
    expect('pieceSource' in state).toBe(false);
    expect('dashboardWidget' in state).toBe(false);
    expect('help' in state).toBe(false);
  });
});

describe('buildUiUrlStateSearch', () => {
  it('produces an empty string for the default state', () => {
    expect(buildUiUrlStateSearch(DEFAULT_STATE)).toBe('');
  });

  it('only serializes non-default values', () => {
    const search = buildUiUrlStateSearch({
      ...DEFAULT_STATE,
      status: 'failed',
      taskId: 12,
    });
    const params = new URLSearchParams(search);
    expect(params.get('status')).toBe('failed');
    expect(params.get('task')).toBe('12');
    expect(Array.from(params.keys()).sort()).toEqual(['status', 'task']);
  });

  it('omits the default dashboardWidget (worker-status) but keeps others', () => {
    expect(
      buildUiUrlStateSearch({ ...DEFAULT_STATE, dashboardWidget: 'worker-status' }),
    ).toBe('');
    expect(
      buildUiUrlStateSearch({ ...DEFAULT_STATE, dashboardWidget: 'gpu' }),
    ).toBe('dashboardWidget=gpu');
  });

  it('round-trips through readUiUrlState', () => {
    const state: UiUrlState = {
      page: 'help',
      repo: 'local/task-9',
      status: 'waiting_human',
      search: 'foo bar',
      sort: 'status',
      scope: 'others',
      detailTab: 'trace',
      mobileTab: 'files',
      taskId: 99,
      section: 'reflection',
      piece: 'research',
      pieceSource: 'builtin',
      dashboardWidget: 'queue',
      help: 'pieces',
    };
    stubLocation(`?${buildUiUrlStateSearch(state)}`);
    expect(readUiUrlState()).toEqual(state);
  });
});

describe('paramsEqualExcept', () => {
  it('considers identical params equal', () => {
    const a = new URLSearchParams('a=1&b=2');
    const b = new URLSearchParams('a=1&b=2');
    expect(paramsEqualExcept(a, b, [])).toBe(true);
  });

  it('ignores key order', () => {
    const a = new URLSearchParams('b=2&a=1');
    const b = new URLSearchParams('a=1&b=2');
    expect(paramsEqualExcept(a, b, [])).toBe(true);
  });

  it('detects value differences', () => {
    const a = new URLSearchParams('a=1');
    const b = new URLSearchParams('a=2');
    expect(paramsEqualExcept(a, b, [])).toBe(false);
  });

  it('skips ignored keys on either side', () => {
    const a = new URLSearchParams('a=1&tab=files');
    const b = new URLSearchParams('a=1&tab=trace');
    expect(paramsEqualExcept(a, b, ['tab'])).toBe(true);
    const c = new URLSearchParams('a=1');
    expect(paramsEqualExcept(a, c, ['tab'])).toBe(true);
  });

  it('detects extra non-ignored keys', () => {
    const a = new URLSearchParams('a=1&extra=x');
    const b = new URLSearchParams('a=1');
    expect(paramsEqualExcept(a, b, [])).toBe(false);
  });

  it('handles repeated keys deterministically', () => {
    const a = new URLSearchParams('k=2&k=1');
    const b = new URLSearchParams('k=1&k=2');
    expect(paramsEqualExcept(a, b, [])).toBe(true);
  });
});

describe('column constants', () => {
  it('every status column has a label', () => {
    for (const col of COLUMN_LIST) {
      expect(COLUMN_LABELS[col], `label for ${col}`).toBeTruthy();
    }
  });
});
