/**
 * lineLayout unit tests
 *
 * 실행: jest lib/layout/__tests__/lineLayout.test.ts
 *
 * jest 설정이 프로젝트에 정식으로 추가되면 바로 실행 가능.
 * 순수 함수만 테스트하므로 DOM / next 의존성 없음.
 */

import {
  computeLayout,
  extractLines,
  mergeThinTracks,
  fitLines,
  nearestLine,
  toTracks,
  toTrackTemplate,
  type LayoutBlockInput,
  LAYOUT_CONSTANTS,
} from '../lineLayout';

const blk = (
  id: string,
  x: number,
  y: number,
  w: number,
  h: number,
  extra: Partial<LayoutBlockInput> = {},
): LayoutBlockInput => ({ id, xPct: x, yPct: y, wPct: w, hPct: h, ...extra });

const approx = (a: number, b: number, tol = 1e-6) => Math.abs(a - b) <= tol;

describe('extractLines', () => {
  test('항상 0 과 1 을 포함한다', () => {
    expect(extractLines([0.3, 0.6], 0.004)).toEqual([0, 0.3, 0.6, 1]);
  });

  test('tolerance 이내 값은 평균 클러스터로 병합된다', () => {
    const out = extractLines([0.3, 0.303], 0.004);
    expect(out.length).toBe(3); // [0, ~0.3015, 1]
    expect(approx(out[1], 0.3015, 1e-3)).toBe(true);
  });

  test('0 과 가까운 값(<=tol) 은 0 에 흡수된다', () => {
    const out = extractLines([0.002, 0.5], 0.004);
    expect(out).toEqual([0, 0.5, 1]);
  });

  test('1 과 가까운 값(>=1-tol) 은 1 에 흡수된다', () => {
    const out = extractLines([0.5, 0.998], 0.004);
    expect(out).toEqual([0, 0.5, 1]);
  });

  test('빈 입력 → [0,1]', () => {
    expect(extractLines([], 0.004)).toEqual([0, 1]);
  });
});

describe('mergeThinTracks', () => {
  test('threshold 미만 내부 트랙은 병합된다', () => {
    const merged = mergeThinTracks([0, 0.499, 0.5, 1], 0.0025);
    expect(merged.length).toBe(3);
    expect(merged[0]).toBe(0);
    expect(merged[2]).toBe(1);
  });

  test('첫 트랙이 얇으면 두 번째 선을 제거한다', () => {
    const merged = mergeThinTracks([0, 0.001, 0.5, 1], 0.0025);
    expect(merged).toEqual([0, 0.5, 1]);
  });

  test('마지막 트랙이 얇으면 뒤에서 두 번째 선을 제거한다', () => {
    const merged = mergeThinTracks([0, 0.5, 0.999, 1], 0.0025);
    expect(merged).toEqual([0, 0.5, 1]);
  });

  test('양 끝 0/1 은 절대 제거되지 않는다', () => {
    const merged = mergeThinTracks([0, 1], 0.5);
    expect(merged).toEqual([0, 1]);
  });
});

describe('nearestLine', () => {
  test('정확히 매칭되는 선을 찾는다', () => {
    const r = nearestLine(0.5, [0, 0.5, 1]);
    expect(r.idx).toBe(1);
    expect(r.err).toBe(0);
  });

  test('가장 가까운 인덱스를 반환한다', () => {
    const r = nearestLine(0.48, [0, 0.5, 1]);
    expect(r.idx).toBe(1);
    expect(approx(r.err, 0.02)).toBe(true);
  });
});

describe('toTracks / toTrackTemplate', () => {
  test('선 [0, 0.3, 1] → cols 2개, weight 0.3 / 0.7', () => {
    const t = toTracks([0, 0.3, 1]);
    expect(t.length).toBe(2);
    expect(approx(t[0].weight, 0.3)).toBe(true);
    expect(approx(t[1].weight, 0.7)).toBe(true);
  });

  test('fr 모드는 minmax(0, Xfr) 형식', () => {
    const t = toTracks([0, 0.3, 1]);
    const tpl = toTrackTemplate(t, 'fr');
    expect(tpl).toContain('minmax(0,');
    expect(tpl).toContain('fr)');
    expect(tpl.split(' ').length).toBeGreaterThanOrEqual(2);
  });

  test('auto 모드는 auto 만 반복', () => {
    const t = toTracks([0, 0.5, 1]);
    expect(toTrackTemplate(t, 'auto')).toBe('auto auto');
  });
});

describe('fitLines — 과분할 재병합', () => {
  test('경계가 너무 많으면 tolerance 를 증가시켜 재추출', () => {
    // 블록 3개인데 서로 0.001 간격의 경계값 10개
    const xs = [0, 0.001, 0.002, 0.003, 0.1, 0.101, 0.5, 0.501, 0.502, 1];
    const r = fitLines(xs, 3, 0.004, 0.02, 1.35, 0.0025);
    // target = 3*2+2 = 8. 원래 선은 최소 ~6~8 개 정도일 것이므로
    // 결과가 target 이하여야 함.
    expect(r.lines.length).toBeLessThanOrEqual(8);
    expect(r.lines[0]).toBe(0);
    expect(r.lines[r.lines.length - 1]).toBe(1);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// computeLayout 통합 테스트 (T1 ~ T15)
// ─────────────────────────────────────────────────────────────────────────────

describe('computeLayout — T1 2×2 균일', () => {
  test('모든 블록이 grid 에 배치되고 cols/rows 가 3/3', () => {
    const r = computeLayout([
      blk('a', 0,   0,   0.5, 0.5),
      blk('b', 0.5, 0,   0.5, 0.5),
      blk('c', 0,   0.5, 0.5, 0.5),
      blk('d', 0.5, 0.5, 0.5, 0.5),
    ]);
    expect(r.xLines).toEqual([0, 0.5, 1]);
    expect(r.yLines).toEqual([0, 0.5, 1]);
    expect(r.cols.length).toBe(2);
    expect(r.rows.length).toBe(2);
    for (const b of r.blocks) expect(b.mode).toBe('grid');
    const a = r.blocks.find((x) => x.blockId === 'a')!;
    expect(a.colStart).toBe(1);
    expect(a.colEnd).toBe(2);
    expect(a.rowStart).toBe(1);
    expect(a.rowEnd).toBe(2);
  });
});

describe('computeLayout — T2 3열 1행', () => {
  test('xLines 가 4개 (0, 1/3, 2/3, 1), 모두 grid', () => {
    const third = 1 / 3;
    const r = computeLayout([
      blk('a', 0,          0, third, 1),
      blk('b', third,      0, third, 1),
      blk('c', 2 * third,  0, third, 1),
    ]);
    expect(r.xLines.length).toBe(4);
    expect(r.yLines).toEqual([0, 1]);
    for (const b of r.blocks) expect(b.mode).toBe('grid');
  });
});

describe('computeLayout — T3 세로 스택', () => {
  test('rows 5개, 모든 블록 grid', () => {
    const blocks = [0, 0.2, 0.4, 0.6, 0.8].map((y, i) =>
      blk(`b${i}`, 0, y, 1, 0.2),
    );
    const r = computeLayout(blocks);
    expect(r.xLines).toEqual([0, 1]);
    expect(r.yLines.length).toBe(6);
    expect(r.rows.length).toBe(5);
    for (const b of r.blocks) expect(b.mode).toBe('grid');
  });
});

describe('computeLayout — T4 0.3% 오차 병합', () => {
  test('0.3% 오차 블록들이 동일한 선으로 병합된다', () => {
    const r = computeLayout([
      blk('a', 0,     0, 0.5, 1),
      blk('b', 0.500, 0, 0.5, 1),
      blk('c', 0.503, 0, 0.5, 1), // 0.3% 차이 → 같은 선
    ]);
    // xLines 는 최소한 0, ~0.5, 1 셋 + 전체 폭을 넘는 값 정도면 OK
    // 핵심: 블록 b 와 c 가 같은 colStart 를 가져야 함
    const b = r.blocks.find((x) => x.blockId === 'b')!;
    const c = r.blocks.find((x) => x.blockId === 'c')!;
    expect(b.mode).toBe('grid');
    expect(c.mode).toBe('grid');
    // 0.003 < baseXTol(0.004) 이라 병합 → 같은 colStart
    expect(c.colStart).toBe(b.colStart);
  });
});

describe('computeLayout — T6 forced overlay', () => {
  test('role=overlay 는 grid 선에 기여하지 않는다', () => {
    const r = computeLayout([
      blk('bg', 0, 0, 1, 1),
      blk('fl', 0.1, 0.1, 0.2, 0.2, { forceOverlay: true }),
    ]);
    // fl 의 경계 0.1, 0.3 은 xLines 에 들어가면 안 됨
    expect(r.xLines).toEqual([0, 1]);
    expect(r.yLines).toEqual([0, 1]);
    const fl = r.blocks.find((x) => x.blockId === 'fl')!;
    expect(fl.mode).toBe('overlay');
    expect(fl.overlayReason).toBe('forced');
  });
});

describe('computeLayout — T7/T8 겹침 15% 경계', () => {
  test('10% 겹침 → 둘 다 grid 유지', () => {
    // 1x1 캔버스, A 가 0~0.5, B 가 0.45~0.95 로 가로 10% 만 겹침, 세로도 10%
    // overlap = 0.05 * 0.05 = 0.0025, 작은 블록 면적 0.25 → 1% → threshold 미만
    const r = computeLayout([
      blk('a', 0,    0,    0.5, 0.5),
      blk('b', 0.45, 0.45, 0.5, 0.5),
    ]);
    // baseTol=0.004 면 0.45/0.5 경계는 별개라 xLines/yLines 가 4개씩 생길 것
    // 두 블록 모두 grid 에 남아야 함. overlap 강등 발생하지 않음.
    const a = r.blocks.find((x) => x.blockId === 'a')!;
    const b = r.blocks.find((x) => x.blockId === 'b')!;
    // a 가 overlap 으로 강등되지 않을 것이라는 점만 보장
    expect(a.mode === 'grid' && b.mode === 'grid').toBe(true);
  });

  test('큰 겹침 → 뒤 블록이 overlay 로 강등', () => {
    // A 와 B 가 거의 동일한 영역 (80% 이상 겹침)
    const r = computeLayout([
      blk('a', 0, 0, 0.5, 0.5),
      blk('b', 0, 0, 0.5, 0.5),
    ]);
    const a = r.blocks.find((x) => x.blockId === 'a')!;
    const b = r.blocks.find((x) => x.blockId === 'b')!;
    expect(a.mode).toBe('grid');
    expect(b.mode).toBe('overlay');
    expect(b.overlayReason).toBe('overlap');
  });
});

describe('computeLayout — T9 invalid span', () => {
  test('wPct=0 블록은 invalid-span overlay 로 빠진다', () => {
    const r = computeLayout([
      blk('a', 0, 0, 1, 1),
      blk('zero', 0.5, 0.5, 0, 0),
    ]);
    const z = r.blocks.find((x) => x.blockId === 'zero')!;
    expect(z.mode).toBe('overlay');
    expect(z.overlayReason).toBe('invalid-span');
  });
});

describe('computeLayout — T13 rowSizing=auto', () => {
  test('rows template 이 auto 들로 구성된다', () => {
    const r = computeLayout(
      [blk('a', 0, 0, 1, 0.5), blk('b', 0, 0.5, 1, 0.5)],
      { rowSizing: 'auto' },
    );
    const tpl = toTrackTemplate(r.rows, 'auto');
    expect(tpl.split(' ').every((s) => s === 'auto')).toBe(true);
  });
});

describe('computeLayout — T14 경계 보장', () => {
  test('블록이 중앙에만 있어도 xLines/yLines 양 끝이 0, 1', () => {
    const r = computeLayout([blk('a', 0.25, 0.25, 0.5, 0.5)]);
    expect(r.xLines[0]).toBe(0);
    expect(r.xLines[r.xLines.length - 1]).toBe(1);
    expect(r.yLines[0]).toBe(0);
    expect(r.yLines[r.yLines.length - 1]).toBe(1);
  });
});

describe('computeLayout — T15 fr weight 비례', () => {
  test('cols weight 가 인접 선 간 거리 비율과 같다', () => {
    const r = computeLayout([
      blk('a', 0,   0, 0.3, 1),
      blk('b', 0.3, 0, 0.7, 1),
    ]);
    expect(r.cols.length).toBe(2);
    expect(approx(r.cols[0].weight, 0.3)).toBe(true);
    expect(approx(r.cols[1].weight, 0.7)).toBe(true);
  });
});

describe('computeLayout — 빈 입력', () => {
  test('blocks 가 빈 배열이면 기본 1x1 grid 를 돌려준다', () => {
    const r = computeLayout([]);
    expect(r.xLines).toEqual([0, 1]);
    expect(r.yLines).toEqual([0, 1]);
    expect(r.cols.length).toBe(1);
    expect(r.rows.length).toBe(1);
    expect(r.blocks.length).toBe(0);
  });
});

describe('LAYOUT_CONSTANTS — 기본값 잠금', () => {
  test('상수 기본값이 설계안과 일치', () => {
    expect(LAYOUT_CONSTANTS.BASE_X_TOLERANCE).toBe(0.004);
    expect(LAYOUT_CONSTANTS.BASE_Y_TOLERANCE).toBe(0.004);
    expect(LAYOUT_CONSTANTS.MAX_TOLERANCE).toBe(0.02);
    expect(LAYOUT_CONSTANTS.TOLERANCE_GROWTH).toBe(1.35);
    expect(LAYOUT_CONSTANTS.THIN_TRACK_THRESHOLD).toBe(0.0025);
    expect(LAYOUT_CONSTANTS.OVERLAP_THRESHOLD).toBe(0.15);
    expect(LAYOUT_CONSTANTS.SNAP_ERROR_MULT).toBe(2);
  });
});
