import { describe, expect, it } from 'vitest';
import {
  computeWindow,
  flattenVisible,
  parseSize,
  setSubtreeExpanded,
  sortNodes,
  toggleExpanded,
} from './treeView';

const file = (id, extra = {}) => ({ id, name: id.split('/').pop(), type: 'file', ...extra });
const folder = (id, children, extra = {}) => ({
  id,
  name: id.split('/').pop(),
  type: 'folder',
  expanded: false,
  children,
  ...extra,
});

describe('flattenVisible', () => {
  it('records depth and skips collapsed subtrees', () => {
    const tree = [
      folder('r/A', [file('r/A/x.txt')], { expanded: true }),
      folder('r/B', [file('r/B/y.txt')]), // collapsed
    ];
    const rows = flattenVisible(tree);
    expect(rows.map((r) => [r.id, r.depth])).toEqual([
      ['r/A', 0],
      ['r/A/x.txt', 1],
      ['r/B', 0],
    ]);
  });
});

describe('computeWindow', () => {
  it('windows the visible slice with overscan and clamps at the ends', () => {
    expect(computeWindow(0, 220, 1000, 22, 5)).toEqual({ start: 0, end: 15 });
    expect(computeWindow(2200, 220, 1000, 22, 5)).toEqual({ start: 95, end: 115 });
    expect(computeWindow(21_500, 220, 1000, 22, 5)).toEqual({ start: 972, end: 993 });
    expect(computeWindow(21_780, 220, 1000, 22, 5)).toEqual({ start: 985, end: 1000 });
  });
});

describe('toggleExpanded', () => {
  it('flips the target and clones only the ancestor chain', () => {
    const target = folder('r/A/B', [file('r/A/B/z.txt')]);
    const sibling = folder('r/C', []);
    const tree = [folder('r/A', [target], { expanded: true }), sibling];
    const next = toggleExpanded(tree, 'r/A/B');
    expect(next).not.toBe(tree);
    expect(next[0]).not.toBe(tree[0]); // ancestor cloned
    expect(next[0].children[0].expanded).toBe(true); // target flipped
    expect(next[1]).toBe(sibling); // untouched branch shares identity
    expect(next[0].children[0].children[0]).toBe(target.children[0]); // grandchildren shared
  });

  it('does not descend into prefix-named siblings', () => {
    const decoy = folder('r/Assets2', [file('r/Assets2/a.txt')]);
    const tree = [decoy, folder('r/Assets', [])];
    const next = toggleExpanded(tree, 'r/Assets');
    expect(next[0]).toBe(decoy);
    expect(next[1].expanded).toBe(true);
  });
});

describe('sortNodes', () => {
  it('sorts by size with folders first and computes status labels once per node', () => {
    const calls = [];
    const labelOf = (node) => {
      calls.push(node.id);
      return '';
    };
    const tree = [
      file('r/big', { size: '2 MB' }),
      file('r/small', { size: '10 KB' }),
      folder('r/dir', []),
    ];
    const sorted = sortNodes(tree, { column: 'size', direction: 'asc' }, labelOf);
    expect(sorted.map((n) => n.id)).toEqual(['r/dir', 'r/small', 'r/big']);

    calls.length = 0;
    sortNodes(tree, { column: 'status', direction: 'asc' }, labelOf);
    expect(new Set(calls).size).toBe(calls.length); // no node labeled twice
  });
});

describe('parseSize', () => {
  it('parses formatted sizes back to bytes', () => {
    expect(parseSize('10 KB')).toBe(10 * 1024);
    expect(parseSize('512 B')).toBe(512);
    expect(parseSize('--')).toBe(0);
  });
});

describe('setSubtreeExpanded', () => {
  const make = () => [
    folder('r/', [
      folder('r/a', [folder('r/a/b', [file('r/a/b/f.txt')]), file('r/a/x.txt')]),
      folder('r/c', [file('r/c/y.txt')]),
    ], { expanded: true }),
  ];

  it('expands the target and every descendant folder', () => {
    const next = setSubtreeExpanded(make(), 'r/a', true);
    const a = next[0].children[0];
    expect(a.expanded).toBe(true);
    expect(a.children[0].expanded).toBe(true); // r/a/b
    expect(next[0].children[1].expanded).toBe(false); // sibling r/c untouched
  });

  it('collapse leaves the target open but closes all descendants', () => {
    const opened = setSubtreeExpanded(make(), 'r/a', true);
    const next = setSubtreeExpanded(opened, 'r/a', false);
    const a = next[0].children[0];
    expect(a.expanded).toBe(true); // unchanged
    expect(a.children[0].expanded).toBe(false); // r/a/b
  });

  it('keeps untouched sibling references and no-ops on files/unknown ids', () => {
    const tree = make();
    const next = setSubtreeExpanded(tree, 'r/a', true);
    expect(next[0].children[1]).toBe(tree[0].children[1]); // r/c same ref
    expect(setSubtreeExpanded(tree, 'r/a/x.txt', true)).toBe(tree);
    expect(setSubtreeExpanded(tree, 'nope', true)).toBe(tree);
  });

  it('expands a root-level folder and its descendants', () => {
    const tree = [
      folder('r/', [folder('r/a', [file('r/a/x.txt')])]),
      folder('r/sibling', [file('r/sibling/y.txt')]),
    ];
    const next = setSubtreeExpanded(tree, 'r/', true);
    expect(next[0].expanded).toBe(true);
    expect(next[0].children[0].expanded).toBe(true); // r/a
    expect(next[1]).toBe(tree[1]); // untouched sibling shares identity
  });
});
