import { describe, it, expect } from 'vitest';
import { filterVisibleItems, flattenWithDepth, getProjection, reorderItems, type BaseNestableItem } from './hooks';

interface TestItem extends BaseNestableItem {
  label: string;
}

const item = (id: number, parent: number | null, label = `item-${id}`): TestItem => ({
  id,
  uuid: `uuid-${id}`,
  parent,
  label,
});

const INDENTATION_WIDTH = 40;

describe('flattenWithDepth', () => {
  it('ルート要素は depth 0 になる', () => {
    const items = [item(1, null), item(2, null)];

    const result = flattenWithDepth(items);

    expect(result.map((i) => i.depth)).toEqual([0, 0]);
  });

  it('親子孫の depth がそれぞれ 0, 1, 2 になる', () => {
    const items = [item(1, null), item(2, 1), item(3, 2)];

    const result = flattenWithDepth(items);

    expect(result.map((i) => i.depth)).toEqual([0, 1, 2]);
  });
});

describe('getProjection', () => {
  it('オフセット0では現在の depth を維持する', () => {
    const items = flattenWithDepth([item(1, null), item(2, null), item(3, null)]);

    const projected = getProjection(items, 3, 2, 0, INDENTATION_WIDTH);

    expect(projected.depth).toBe(0);
    expect(projected.parentId).toBeNull();
  });

  it('直前のアイテムの子になれる分だけ右にオフセットすると depth が1つ増え、直前アイテムが親になる', () => {
    const items = flattenWithDepth([item(1, null), item(2, null), item(3, null)]);

    // 3 を 2 の位置にドラッグしつつ、右方向に1階層分オフセットする
    const projected = getProjection(items, 3, 2, INDENTATION_WIDTH, INDENTATION_WIDTH);

    expect(projected.depth).toBe(1);
    expect(projected.parentId).toBe(1);
  });

  it('直前のアイテムが存在しない(先頭に移動する)場合は depth 0 に固定される', () => {
    const items = flattenWithDepth([item(1, null), item(2, null), item(3, null)]);

    const projected = getProjection(items, 3, 1, INDENTATION_WIDTH * 3, INDENTATION_WIDTH);

    expect(projected.depth).toBe(0);
    expect(projected.parentId).toBeNull();
  });

  it('次のアイテムより深くネストしようとしても、次のアイテムの depth 未満にはできない(minDepth制約)', () => {
    const items = flattenWithDepth([item(1, null), item(2, 1), item(3, null)]);

    // 3 を 2 と 1(祖先1) の間に移動しようとしても、次アイテム(1, depth 0)未満にはならない
    const projected = getProjection(items, 3, 2, -INDENTATION_WIDTH * 5, INDENTATION_WIDTH);

    expect(projected.depth).toBeGreaterThanOrEqual(0);
  });

  it('親の直下に既にいる場合、同じ depth のままなら親IDは前アイテムの親と同じになる', () => {
    const items = flattenWithDepth([item(1, null), item(2, 1), item(3, 1)]);

    const projected = getProjection(items, 3, 2, 0, INDENTATION_WIDTH);

    expect(projected.depth).toBe(1);
    expect(projected.parentId).toBe(1);
  });
});

describe('reorderItems', () => {
  it('activeId の parent を更新しつつ配列内の位置を overId の位置へ移動する', () => {
    const items = [item(1, null), item(2, null), item(3, null)];

    const result = reorderItems(items, 3, 1, null);

    expect(result.map((i) => i.id)).toEqual([3, 1, 2]);
  });

  it('parentId を指定すると対象アイテムの parent が更新される', () => {
    const items = [item(1, null), item(2, null), item(3, null)];

    const result = reorderItems(items, 3, 2, 1);

    expect(result.find((i) => i.id === 3)?.parent).toBe(1);
  });

  it('activeId, overId が見つからない場合は変更しない', () => {
    const items = [item(1, null), item(2, null)];

    const result = reorderItems(items, 999, 1, null);

    expect(result).toEqual(items);
  });
});

describe('filterVisibleItems', () => {
  it('collapsedIds が空なら全アイテムを表示する', () => {
    const items = flattenWithDepth([item(1, null), item(2, 1), item(3, null)]);

    const result = filterVisibleItems(items, new Set());

    expect(result.map((i) => i.id)).toEqual([1, 2, 3]);
  });

  it('折りたたまれたアイテムの子を非表示にする', () => {
    const items = flattenWithDepth([item(1, null), item(2, 1), item(3, null)]);

    const result = filterVisibleItems(items, new Set([1]));

    expect(result.map((i) => i.id)).toEqual([1, 3]);
  });

  it('折りたたまれたアイテムの孫も非表示にする(祖先を辿る)', () => {
    const items = flattenWithDepth([item(1, null), item(2, 1), item(3, 2), item(4, null)]);

    const result = filterVisibleItems(items, new Set([1]));

    expect(result.map((i) => i.id)).toEqual([1, 4]);
  });

  it('折りたたまれていないアイテムの子は表示されたままになる', () => {
    const items = flattenWithDepth([item(1, null), item(2, 1), item(3, null), item(4, 3)]);

    const result = filterVisibleItems(items, new Set([3]));

    expect(result.map((i) => i.id)).toEqual([1, 2, 3]);
  });
});
