/**
 * Tests for the TransformMatrix module (packages/core/src/canvas/matrix.ts).
 *
 * TransformMatrix is a lightweight 3x3 matrix library for 2D affine transforms.
 * It handles translation, rotation, scaling, inversion, and point mapping.
 * Used by the renderer for world-space coordinate computation and culling.
 */
import { describe, test, expect } from 'bun:test'

import { TransformMatrix } from '@open-pencil/core'

import { expectDefined } from '#tests/helpers/assert'

describe('TransformMatrix.identity', () => {
  test('produces a 3x3 identity matrix', () => {
    const id = TransformMatrix.identity()
    expect(id).toEqual([1, 0, 0, 0, 1, 0, 0, 0, 1])
  })
})

describe('TransformMatrix.multiply', () => {
  test('identity * identity = identity', () => {
    const id = TransformMatrix.identity()
    expect(TransformMatrix.multiply(id, id)).toEqual(id)
  })

  test('multiply with no args returns identity', () => {
    expect(TransformMatrix.multiply()).toEqual([1, 0, 0, 0, 1, 0, 0, 0, 1])
  })

  test('translate then rotate preserves translation', () => {
    const t = TransformMatrix.translated(10, 20)
    const r = TransformMatrix.rotated(0) // no rotation
    const result = TransformMatrix.multiply(t, r)
    // Translation should be preserved
    expect(result[2]).toBeCloseTo(10, 10)
    expect(result[5]).toBeCloseTo(20, 10)
  })

  test('two translations compose additively', () => {
    const t1 = TransformMatrix.translated(10, 20)
    const t2 = TransformMatrix.translated(5, 15)
    const result = TransformMatrix.multiply(t1, t2)
    // Combined translation: (15, 35)
    expect(result[2]).toBeCloseTo(15, 10)
    expect(result[5]).toBeCloseTo(35, 10)
  })

  test('two scalings compose multiplicatively', () => {
    const s1 = TransformMatrix.scaled(2, 3)
    const s2 = TransformMatrix.scaled(4, 5)
    const result = TransformMatrix.multiply(s1, s2)
    // Combined scale: (8, 15)
    expect(result[0]).toBeCloseTo(8, 10)
    expect(result[4]).toBeCloseTo(15, 10)
  })
})

describe('TransformMatrix.translated', () => {
  test('produces correct translation matrix', () => {
    const t = TransformMatrix.translated(100, 200)
    expect(t).toEqual([1, 0, 100, 0, 1, 200, 0, 0, 1])
  })

  test('zero translation is identity', () => {
    expect(TransformMatrix.translated(0, 0)).toEqual(TransformMatrix.identity())
  })
})

describe('TransformMatrix.rotated', () => {
  test('zero rotation is identity', () => {
    const r = TransformMatrix.rotated(0)
    expect(r[0]).toBeCloseTo(1, 10)
    expect(r[1]).toBeCloseTo(0, 10)
    expect(r[3]).toBeCloseTo(0, 10)
    expect(r[4]).toBeCloseTo(1, 10)
  })

  test('90° rotation produces correct matrix', () => {
    const r = TransformMatrix.rotated(Math.PI / 2)
    expect(r[0]).toBeCloseTo(0, 10)
    expect(r[1]).toBeCloseTo(-1, 10)
    expect(r[3]).toBeCloseTo(1, 10)
    expect(r[4]).toBeCloseTo(0, 10)
  })

  test('180° rotation negates both axes', () => {
    const r = TransformMatrix.rotated(Math.PI)
    expect(r[0]).toBeCloseTo(-1, 10)
    expect(r[4]).toBeCloseTo(-1, 10)
  })

  test('rotation around arbitrary pivot point', () => {
    // Rotate 90° around (50, 50)
    const r = TransformMatrix.rotated(Math.PI / 2, 50, 50)
    // Map (50, 50) through rotation → should stay at (50, 50)
    const pt = TransformMatrix.mapPoint(r, { x: 50, y: 50 })
    expect(pt.x).toBeCloseTo(50, 10)
    expect(pt.y).toBeCloseTo(50, 10)
    // Map (100, 50) → (50, 100) for 90° around (50,50)
    const pt2 = TransformMatrix.mapPoint(r, { x: 100, y: 50 })
    expect(pt2.x).toBeCloseTo(50, 10)
    expect(pt2.y).toBeCloseTo(100, 10)
  })
})

describe('TransformMatrix.scaled', () => {
  test('uniform scaling from origin', () => {
    const s = TransformMatrix.scaled(2, 3)
    expect(s[0]).toBe(2)
    expect(s[4]).toBe(3)
    expect(s[2]).toBe(0)
    expect(s[5]).toBe(0)
  })

  test('scaling around pivot point', () => {
    // Scale 2x around (50, 50)
    const s = TransformMatrix.scaled(2, 2, 50, 50)
    // Map (50, 50) → (50, 50) — pivot is invariant
    const pt = TransformMatrix.mapPoint(s, { x: 50, y: 50 })
    expect(pt.x).toBeCloseTo(50, 10)
    expect(pt.y).toBeCloseTo(50, 10)
    // Map (60, 60) → (70, 70) — 2x scaling around (50,50): (60-50)*2+50 = 70
    const pt2 = TransformMatrix.mapPoint(s, { x: 60, y: 60 })
    expect(pt2.x).toBeCloseTo(70, 10)
    expect(pt2.y).toBeCloseTo(70, 10)
  })

  test('scale of 1 is identity', () => {
    const s = TransformMatrix.scaled(1, 1)
    expect(s).toEqual(TransformMatrix.identity())
  })
})

describe('TransformMatrix.invert', () => {
  test('inverse of identity is identity', () => {
    const id = TransformMatrix.identity()
    const inv = TransformMatrix.invert(id)
    const inverse = expectDefined(inv, 'inverse matrix')
    expect(inverse).toEqual(id)
  })

  test('inverse of translation is negative translation', () => {
    const t = TransformMatrix.translated(10, 20)
    const inv = TransformMatrix.invert(t)
    const inverse = expectDefined(inv, 'inverse matrix')
    expect(inverse[2]).toBeCloseTo(-10, 10)
    expect(inverse[5]).toBeCloseTo(-20, 10)
  })

  test('inverse of rotation is negative rotation', () => {
    const r = TransformMatrix.rotated(Math.PI / 4)
    const inv = TransformMatrix.invert(r)
    const inverse = expectDefined(inv, 'inverse matrix')
    // Rotation by -π/4
    const expected = TransformMatrix.rotated(-Math.PI / 4)
    for (let i = 0; i < 9; i++) {
      expect(inverse[i]).toBeCloseTo(expected[i], 10)
    }
  })

  test('compose with inverse gives identity', () => {
    const m = TransformMatrix.multiply(
      TransformMatrix.translated(10, 20),
      TransformMatrix.rotated(Math.PI / 6)
    )
    const inv = TransformMatrix.invert(m)
    const inverse = expectDefined(inv, 'inverse matrix')
    const product = TransformMatrix.multiply(m, inverse)
    for (let i = 0; i < 9; i++) {
      expect(product[i]).toBeCloseTo(TransformMatrix.identity()[i], 10)
    }
  })

  test('singular matrix returns null', () => {
    const singular = [0, 0, 0, 0, 0, 0, 0, 0, 0]
    expect(TransformMatrix.invert(singular)).toBeNull()
  })
})

describe('TransformMatrix.mapPoints', () => {
  test('identity maps points to themselves', () => {
    const id = TransformMatrix.identity()
    expect(TransformMatrix.mapPoints(id, [10, 20])).toEqual([10, 20])
  })

  test('translation adds to coordinates', () => {
    const t = TransformMatrix.translated(5, 10)
    expect(TransformMatrix.mapPoints(t, [0, 0])).toEqual([5, 10])
    expect(TransformMatrix.mapPoints(t, [100, 200])).toEqual([105, 210])
  })

  test('multiple points mapped correctly', () => {
    const s = TransformMatrix.scaled(2, 3)
    const result = TransformMatrix.mapPoints(s, [1, 2, 3, 4])
    expect(result[0]).toBeCloseTo(2, 10)
    expect(result[1]).toBeCloseTo(6, 10)
    expect(result[2]).toBeCloseTo(6, 10)
    expect(result[3]).toBeCloseTo(12, 10)
  })

  test('odd-length array throws error', () => {
    expect(() => TransformMatrix.mapPoints(TransformMatrix.identity(), [1, 2, 3])).toThrow(
      'mapPoints requires even length'
    )
  })
})

describe('TransformMatrix.mapPoint', () => {
  test('returns Vector object', () => {
    const id = TransformMatrix.identity()
    const result = TransformMatrix.mapPoint(id, { x: 42, y: 99 })
    expect(result).toEqual({ x: 42, y: 99 })
  })

  test('rotation by 90° maps correctly', () => {
    const r = TransformMatrix.rotated(Math.PI / 2)
    const result = TransformMatrix.mapPoint(r, { x: 1, y: 0 })
    expect(result.x).toBeCloseTo(0, 10)
    expect(result.y).toBeCloseTo(1, 10)
  })
})

describe('composed transforms (realistic world matrix chains)', () => {
  test('translate then scale: point (0,0) maps to translated position', () => {
    // Simulates a node at position (100, 200) with no rotation/flip
    // getNodeLocalMatrix: translate(100,200) * translate(cx,cy) * translate(-cx,-cy)
    // The inner translate(cx,cy) * translate(-cx,-cy) cancels out
    // So the result is just translate(100, 200)
    const cx = 50 // width/2
    const cy = 30 // height/2
    let m = TransformMatrix.identity()
    m = TransformMatrix.multiply(m, TransformMatrix.translated(100, 200))
    m = TransformMatrix.multiply(m, TransformMatrix.translated(cx, cy))
    m = TransformMatrix.multiply(m, TransformMatrix.translated(-cx, -cy))

    const origin = TransformMatrix.mapPoints(m, [0, 0])
    expect(origin[0]).toBeCloseTo(100, 10)
    expect(origin[1]).toBeCloseTo(200, 10)
  })

  test('translate then rotate 90° around center: corners map correctly', () => {
    // A 100x100 node at position (0, 0) rotated 90° around center (50, 50)
    const cx = 50
    const cy = 50
    const rad = Math.PI / 2

    let m = TransformMatrix.identity()
    m = TransformMatrix.multiply(m, TransformMatrix.translated(0, 0)) // position
    m = TransformMatrix.multiply(m, TransformMatrix.translated(cx, cy)) // pivot to center
    m = TransformMatrix.multiply(m, TransformMatrix.rotated(rad)) // rotate
    m = TransformMatrix.multiply(m, TransformMatrix.translated(-cx, -cy)) // pivot back

    // Origin (0,0) rotated 90° around (50,50) → (100, 0)
    const origin = TransformMatrix.mapPoints(m, [0, 0])
    expect(origin[0]).toBeCloseTo(100, 10)
    expect(origin[1]).toBeCloseTo(0, 10)

    // Center (50,50) should be invariant
    const center = TransformMatrix.mapPoints(m, [50, 50])
    expect(center[0]).toBeCloseTo(50, 10)
    expect(center[1]).toBeCloseTo(50, 10)
  })

  test('translate + rotate + scale: full chain produces correct world position', () => {
    // A 200x100 node at position (300, 400), rotated 45°, flipped X
    const cx = 100 // width/2
    const cy = 50 // height/2
    const rad = (45 * Math.PI) / 180

    let m = TransformMatrix.identity()
    m = TransformMatrix.multiply(m, TransformMatrix.translated(300, 400)) // position
    m = TransformMatrix.multiply(m, TransformMatrix.translated(cx, cy)) // pivot to center
    m = TransformMatrix.multiply(m, TransformMatrix.scaled(-1, 1)) // flipX
    m = TransformMatrix.multiply(m, TransformMatrix.rotated(rad)) // rotate 45°
    m = TransformMatrix.multiply(m, TransformMatrix.translated(-cx, -cy)) // pivot back

    // The center (100, 50) should map to (300+100, 400+50) = (400, 450)
    // because rotation around center leaves center invariant (flipX doesn't affect center either)
    const center = TransformMatrix.mapPoints(m, [100, 50])
    expect(center[0]).toBeCloseTo(400, 5)
    expect(center[1]).toBeCloseTo(450, 5)

    // The origin (0,0) should be rotated and flipped
    const origin = TransformMatrix.mapPoints(m, [0, 0])
    // Verify it's NOT at (300, 400) — it should be transformed
    expect(origin[0]).not.toBeCloseTo(300, 1)
  })

  test('two-level nesting: parent translate + child translate composes correctly', () => {
    // Parent at (100, 200), child at (50, 30) relative to parent
    // Child's world position should be (150, 230)
    const parentMatrix = TransformMatrix.multiply(
      TransformMatrix.identity(),
      TransformMatrix.translated(100, 200)
    )

    // Child's local matrix (simplified, no rotation/flip, 80x60 node)
    const cx = 40
    const cy = 30
    let childLocal = TransformMatrix.identity()
    childLocal = TransformMatrix.multiply(childLocal, TransformMatrix.translated(50, 30))
    childLocal = TransformMatrix.multiply(childLocal, TransformMatrix.translated(cx, cy))
    childLocal = TransformMatrix.multiply(childLocal, TransformMatrix.translated(-cx, -cy))

    // World matrix = parent * child
    const worldMatrix = TransformMatrix.multiply(parentMatrix, childLocal)

    // Origin of child in world space
    const origin = TransformMatrix.mapPoints(worldMatrix, [0, 0])
    expect(origin[0]).toBeCloseTo(150, 10)
    expect(origin[1]).toBeCloseTo(230, 10)

    // Center of child (40, 30) in world space should be (150+40, 230+30) = (190, 260)
    const center = TransformMatrix.mapPoints(worldMatrix, [40, 30])
    expect(center[0]).toBeCloseTo(190, 10)
    expect(center[1]).toBeCloseTo(260, 10)
  })

  test('inverse of world matrix maps world position back to local', () => {
    // Node at (100, 200) with 30° rotation
    const rad = (30 * Math.PI) / 180
    const cx = 50
    const cy = 25

    let m = TransformMatrix.identity()
    m = TransformMatrix.multiply(m, TransformMatrix.translated(100, 200))
    m = TransformMatrix.multiply(m, TransformMatrix.translated(cx, cy))
    m = TransformMatrix.multiply(m, TransformMatrix.rotated(rad))
    m = TransformMatrix.multiply(m, TransformMatrix.translated(-cx, -cy))

    const inv = TransformMatrix.invert(m)
    const inverse = expectDefined(inv, 'inverse matrix')

    // Map origin to world, then back to local
    const worldOrigin = TransformMatrix.mapPoints(m, [0, 0])
    const localOrigin = TransformMatrix.mapPoints(inverse, worldOrigin)
    expect(localOrigin[0]).toBeCloseTo(0, 8)
    expect(localOrigin[1]).toBeCloseTo(0, 8)

    // Map center to world, then back
    const worldCenter = TransformMatrix.mapPoints(m, [50, 25])
    const localCenter = TransformMatrix.mapPoints(inverse, worldCenter)
    expect(localCenter[0]).toBeCloseTo(50, 8)
    expect(localCenter[1]).toBeCloseTo(25, 8)
  })
})
