// Zellzustände aus einer Arbeitsmappe holen (Kap. 6.3).
//
// Die Doubles hier bilden nach, wie ExcelJS Formeln, Fehlerwerte und verbundene
// Bereiche ablegt. Beide Eigenheiten haben beim ersten Lauf gegen die echten
// Dateien zugeschlagen: der Fehlerwert steckt verschachtelt im Ergebnis, und
// der Anzeigetext einer verbundenen Zelle wirft, wenn die führende Zelle leer ist.

import { describe, it, expect } from 'vitest'
import { columnLetter, hiddenColumns, readCells, toSnapshot, type WorksheetLike } from '../workbook-cells'
import { classifyCell } from '../cell-state'

describe('columnLetter', () => {
  it('rechnet die Spaltennummer in den Buchstaben um', () => {
    expect(columnLetter(1)).toBe('A')
    expect(columnLetter(26)).toBe('Z')
    expect(columnLetter(27)).toBe('AA')
    expect(columnLetter(46)).toBe('AT')
    expect(columnLetter(703)).toBe('AAA')
  })
})

describe('toSnapshot', () => {
  it('trennt Formel und Ergebnis', () => {
    const s = toSnapshot({ value: { formula: 'A1*B1', result: 42 } }, 'Blatt', 'C1')
    expect(s).toMatchObject({ formula: 'A1*B1', value: 42 })
    expect(classifyCell(s)).toBe('formula_and_cached')
  })

  it('nimmt die gemeinsam genutzte Formel als Formel', () => {
    // Excel speichert wiederholte Formeln nur einmal; die übrigen Zellen
    // verweisen darauf. Ohne diesen Fall gälten sie als feste Zahlen.
    const s = toSnapshot({ value: { sharedFormula: 'AP14', result: 250_000 } }, 'Blatt', 'AP88')
    expect(classifyCell(s)).toBe('formula_and_cached')
  })

  it('holt den Fehlerwert aus dem verschachtelten Ergebnis', () => {
    // So legt ExcelJS #N/A ab: nicht als Zeichenkette, sondern als Objekt im
    // Ergebnis. Ungefiltert wäre die Zelle eine gewöhnliche Formelzelle.
    const s = toSnapshot({ value: { sharedFormula: 'AT15', result: { error: '#N/A' } } }, 'Blatt', 'AT20')
    expect(s.value).toBe('#N/A')
    expect(classifyCell(s)).toBe('error')
  })

  it('bringt ein Datum auf eine Zahl', () => {
    const s = toSnapshot({ value: new Date('2022-05-13T00:00:00.000Z') }, 'Blatt', 'C8')
    expect(typeof s.value).toBe('number')
  })

  it('bringt formatierten Text auf seinen Inhalt', () => {
    const s = toSnapshot({ value: { richText: [{ text: 'Ange' }, { text: 'bot' }] } }, 'Blatt', 'B8')
    expect(s.value).toBe('Angebot')
  })

  it('scheitert nicht am Anzeigetext einer verbundenen Zelle', () => {
    // `text` ist bei ExcelJS berechnet und wirft, wenn die führende Zelle des
    // verbundenen Bereichs leer ist. Ein Absturz beim Einlesen wäre die
    // schlechteste aller Antworten.
    const boese = {
      value: 5,
      get text(): string {
        throw new TypeError("Cannot read properties of null (reading 'toString')")
      },
    }
    expect(() => toSnapshot(boese, 'Blatt', 'D8')).not.toThrow()
    expect(toSnapshot(boese, 'Blatt', 'D8').text).toBeNull()
  })
})

/** Blatt-Double: Zeilen als Listen von Zellen, Spalten 1-basiert. */
function sheet(rows: Record<number, unknown[]>, columns?: Array<{ letter?: string; hidden?: boolean }>): WorksheetLike {
  const numbers = Object.keys(rows).map(Number)
  return {
    name: 'Blatt',
    rowCount: numbers.length === 0 ? 0 : Math.max(...numbers),
    columns,
    getRow(r) {
      return {
        eachCell(_opts, cb) {
          ;(rows[r] ?? []).forEach((c, i) => {
            if (c === undefined) return
            cb(c as { value: unknown }, i + 1)
          })
        },
      }
    },
  }
}

describe('readCells', () => {
  it('liest den verlangten Bereich mit Fundstellen', () => {
    const ws = sheet({ 1: [{ value: 'A' }, { value: 'B' }], 2: [{ value: 1 }, { value: 2 }] })
    const cells = readCells(ws, { fromRow: 1, toRow: 2 })
    expect(cells.map((c) => c.cell)).toEqual(['A1', 'B1', 'A2', 'B2'])
  })

  it('beschränkt sich auf die verlangten Spalten', () => {
    const ws = sheet({ 1: [{ value: 'A' }, { value: 'B' }, { value: 'C' }] })
    expect(readCells(ws, { fromRow: 1, toRow: 1, columns: [2] }).map((c) => c.cell)).toEqual(['B1'])
  })

  it('liest nicht über das Blattende hinaus', () => {
    const ws = sheet({ 1: [{ value: 'A' }] })
    expect(readCells(ws, { fromRow: 1, toRow: 500 })).toHaveLength(1)
  })

  it('meldet einen verbundenen Bereich nur einmal', () => {
    // Ein verbundener Bereich trägt seinen Wert in jeder überspannten Zelle.
    // Ungefiltert meldet jeder Detektor denselben Befund so oft, wie der
    // Bereich Spalten breit ist — im Anlassfall dreimal dasselbe Datum.
    const ws = sheet({
      8: [
        { value: '07.04.2025', isMerged: true, master: { address: 'A8' }, address: 'A8' },
        { value: '07.04.2025', isMerged: true, master: { address: 'A8' }, address: 'B8' },
        { value: '07.04.2025', isMerged: true, master: { address: 'A8' }, address: 'C8' },
      ],
    })
    expect(readCells(ws, { fromRow: 8, toRow: 8 }).map((c) => c.cell)).toEqual(['A8'])
  })
})

describe('hiddenColumns', () => {
  it('nennt die ausgeblendeten Spalten mit ihrem Buchstaben', () => {
    const ws = sheet({}, [{ letter: 'A' }, { letter: 'B', hidden: true }, { hidden: true }])
    expect(hiddenColumns(ws)).toEqual(['B', 'C'])
  })

  it('gibt eine leere Liste zurück, wenn nichts ausgeblendet ist', () => {
    expect(hiddenColumns(sheet({}, [{ letter: 'A' }]))).toEqual([])
    expect(hiddenColumns(sheet({}))).toEqual([])
  })
})
