/**
 * Unit tests for CCLiveTranscriptionShapeUtil
 *
 * Intended production location:
 *   src/utils/tldraw/cc-base/cc-transcription/CCLiveTranscriptionShapeUtil.test.ts
 *
 * Run from classroom-copilot-app root:
 *   npx vitest run transcription-shape
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { Editor, createShapeId, createTLStore } from '@tldraw/tldraw'
import { CCLiveTranscriptionShapeUtil, type CCLiveTranscriptionShape, type TranscriptionSegment } from './CCLiveTranscriptionShapeUtil'
import { CCSlideShapeUtil } from '../cc-slideshow/CCSlideShapeUtil'
import { CCSlideShowShapeUtil } from '../cc-slideshow/CCSlideShowShapeUtil'
import { getDefaultCCLiveTranscriptionProps } from '../cc-props'

// ─── Mock TranscriptionManager ──────────────────────────────────────────────
// Must be hoisted before the module under test is imported in a real project.
// Here we patch the module directly since we control the import order.

// Declare symbols before vi.mock so hoisting/factory capture stays consistent.
const mockStartTranscription = () => {}
const mockStopTranscription  = () => {}

vi.mock('./TranscriptionManager', () => {
  const start = vi.fn()
  const stop  = vi.fn()
  Object.assign(globalThis, { __mockStartTranscription: start, __mockStopTranscription: stop })
  return {
    TranscriptionManager: {
      getManager: vi.fn().mockReturnValue({ startTranscription: start, stopTranscription: stop }),
    },
  }
})

const sm = () => (globalThis as unknown as Record<string, any>).__mockStartTranscription
const st = () => (globalThis as unknown as Record<string, any>).__mockStopTranscription

const clearMocks = () => { sm().mockClear(); st().mockClear() }

// Provide a minimal matchMedia so tldraw's UserPreferencesManager
// can call addEventListener/removeEventListener in jsdom.
if (typeof globalThis.matchMedia !== 'function') {
  vi.stubGlobal('matchMedia', () => ({
    matches: false,
    media: '',
    onchange: null,
    addEventListener: () => {},
    removeEventListener: () => {},
    addListener: () => {},
    removeListener: () => {},
    dispatchEvent: () => false,
  } as unknown as MediaQueryList))
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

/** Minimal shape utils needed to boot the Editor for transcription tests. */
const shapeUtils = [CCLiveTranscriptionShapeUtil, CCSlideShapeUtil, CCSlideShowShapeUtil]

function makeEditor(): Editor {
  const store = createTLStore({ shapeUtils, bindingUtils: [] })
  return new Editor({
    shapeUtils,
    bindingUtils: [],
    tools: [],
    store,
    getContainer: () => document.body,
  })
}

/** Create a transcription shape with optional prop overrides and return its id. */
function createTranscriptionShape(
  editor: Editor,
  overrides: Partial<CCLiveTranscriptionShape['props']> = {}
) {
  const id = createShapeId()
  editor.createShape<CCLiveTranscriptionShape>({
    id,
    type: 'cc-live-transcription',
    x: 0,
    y: 0,
    props: {
      ...getDefaultCCLiveTranscriptionProps(),
      ...overrides,
    } as CCLiveTranscriptionShape['props'],
  })
  return id
}

/** Typed getter for the shape record. */
function getShape(editor: Editor, id: ReturnType<typeof createShapeId>) {
  return editor.getShape<CCLiveTranscriptionShape>(id)!
}

/** Build a finished TranscriptionSegment fixture. */
function makeSegment(text: string, overrides: Partial<TranscriptionSegment> = {}): TranscriptionSegment {
  return {
    id: crypto.randomUUID(),
    text,
    completed: true,
    start: '0.000',
    end: '1.000',
    ...overrides,
  }
}

// ─── Test Suite ──────────────────────────────────────────────────────────────

describe('CCLiveTranscriptionShapeUtil', () => {
  let editor: Editor
  let util: CCLiveTranscriptionShapeUtil

  beforeEach(() => {
    editor = makeEditor()
    util   = editor.getShapeUtil<CCLiveTranscriptionShapeUtil>('cc-live-transcription')
    clearMocks()
  })

  afterEach(() => {
    editor.dispose()
  })

  // ── getDefaultProps ─────────────────────────────────────────────────────

  describe('getDefaultProps()', () => {
    it('returns the expected default structure', () => {
      const defaults = util.getDefaultProps()

      expect(defaults).toMatchObject({
        isRecording:           false,
        segments:              [],
        currentSegment:        undefined,
        lastProcessedSegment:  undefined,
      })
      // Base props present
      expect(typeof defaults.title).toBe('string')
      expect(typeof defaults.w).toBe('number')
      expect(typeof defaults.h).toBe('number')
      expect(typeof defaults.headerColor).toBe('string')
      expect(typeof defaults.backgroundColor).toBe('string')
      expect(defaults.isLocked).toBe(false)
    })

    it('segments array is empty, not null or undefined', () => {
      const { segments } = util.getDefaultProps()
      expect(Array.isArray(segments)).toBe(true)
      expect(segments).toHaveLength(0)
    })
  })

  // ── updateText — unconfirmed (live partial) ─────────────────────────────

  describe('updateText() — unconfirmed segment (isConfirmed = false)', () => {
    it('updates only currentSegment, does not touch the segments array', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })

      util.updateText(id, 'hello wor', false, { start: 0, end: 0.8 })

      const shape = getShape(editor, id)
      expect(shape.props.segments).toHaveLength(0)
      expect(shape.props.currentSegment).toMatchObject({
        text: 'hello wor',
        completed: false,
      })
    })

    it('overwrites currentSegment when called again with new partial text', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })

      util.updateText(id, 'hello wor', false, { start: 0, end: 0.8 })
      util.updateText(id, 'hello world',  false, { start: 0, end: 1.2 })

      const shape = getShape(editor, id)
      expect(shape.props.currentSegment?.text).toBe('hello world')
      expect(shape.props.segments).toHaveLength(0)
    })

    it('does NOT call updateShape when the partial text is unchanged', () => {
      const id = createTranscriptionShape(editor, {
        isRecording: true,
        currentSegment: makeSegment('same text', { completed: false }),
      })

      const spy = vi.spyOn(editor, 'updateShape')
      util.updateText(id, 'same text', false, { start: 0, end: 1.0 })

      expect(spy).not.toHaveBeenCalled()
      spy.mockRestore()
    })
  })

  // ── updateText — confirmed (completed) segment ─────────────────────────

  describe('updateText() — confirmed segment (isConfirmed = true)', () => {
    it('appends to segments array when text is new', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })

      util.updateText(id, 'First sentence.', true, { start: 0, end: 2.1 })

      const shape = getShape(editor, id)
      expect(shape.props.segments).toHaveLength(1)
      expect(shape.props.segments[0]).toMatchObject({
        text:      'First sentence.',
        completed: true,
        start:     '0.000',
        end:       '2.100',
      })
    })

    it('updates lastProcessedSegment to the confirmed text', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })

      util.updateText(id, 'Second sentence.', true, { start: 2, end: 4 })

      expect(getShape(editor, id).props.lastProcessedSegment).toBe('Second sentence.')
    })

    it('clears currentSegment when its text matches the confirmed text', () => {
      const id = createTranscriptionShape(editor, {
        isRecording: true,
        currentSegment: makeSegment('Final words.', { completed: false }),
      })

      util.updateText(id, 'Final words.', true, { start: 5, end: 7 })

      expect(getShape(editor, id).props.currentSegment).toBeUndefined()
    })

    it('preserves currentSegment when its text differs from the confirmed text', () => {
      const id = createTranscriptionShape(editor, {
        isRecording: true,
        currentSegment: makeSegment('still typing...', { completed: false }),
      })

      util.updateText(id, 'Earlier sentence confirmed.', true, { start: 0, end: 3 })

      expect(getShape(editor, id).props.currentSegment?.text).toBe('still typing...')
    })

    it('DEDUPLICATION — does not add a segment whose text is already in segments[]', () => {
      const existingSegment = makeSegment('Duplicate text.')
      const id = createTranscriptionShape(editor, {
        isRecording: true,
        segments: [existingSegment],
        lastProcessedSegment: 'Duplicate text.',
      })

      util.updateText(id, 'Duplicate text.', true, { start: 10, end: 12 })

      const shape = getShape(editor, id)
      expect(shape.props.segments).toHaveLength(1)
      expect(shape.props.segments[0].id).toBe(existingSegment.id)
    })

    it('DEDUPLICATION — blocks re-add even when lastProcessedSegment differs (isDuplicate text check)', () => {
      const existingSegment = makeSegment('Already confirmed.')
      const id = createTranscriptionShape(editor, {
        isRecording: true,
        segments: [existingSegment],
        lastProcessedSegment: undefined,
      })

      util.updateText(id, 'Already confirmed.', true, { start: 15, end: 17 })

      expect(getShape(editor, id).props.segments).toHaveLength(1)
    })

    it('accumulates multiple unique confirmed segments in order', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })

      util.updateText(id, 'Sentence one.',   true, { start: 0,  end: 2  })
      util.updateText(id, 'Sentence two.',   true, { start: 2,  end: 4  })
      util.updateText(id, 'Sentence three.', true, { start: 4,  end: 6  })

      const { segments } = getShape(editor, id).props
      expect(segments).toHaveLength(3)
      expect(segments.map((s) => s.text)).toEqual([
        'Sentence one.',
        'Sentence two.',
        'Sentence three.',
      ])
    })

    it('assigns unique ids to each confirmed segment', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })

      util.updateText(id, 'Alpha.', true, { start: 0, end: 1 })
      util.updateText(id, 'Beta.',  true, { start: 1, end: 2 })

      const { segments } = getShape(editor, id).props
      expect(segments[0].id).not.toBe(segments[1].id)
    })

    it('formats numeric start/end timestamps to 3 decimal places', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })

      util.updateText(id, 'Timestamped.', true, { start: 1.1, end: 2.5678 })

      const seg = getShape(editor, id).props.segments[0]
      expect(seg.start).toBe('1.100')
      expect(seg.end).toBe('2.568')
    })
  })

  // ── toggleRecording ────────────────────────────────────────────────────

  describe('toggleRecording() — via private accessor', () => {
    // TypeScript private methods are still callable at runtime; we use
    // bracket notation to bypass the access check cleanly in tests.
    const toggle = (u: CCLiveTranscriptionShapeUtil, shape: CCLiveTranscriptionShape) =>
      (u as unknown as { toggleRecording(s: CCLiveTranscriptionShape): void }).toggleRecording(shape)

    it('STARTING — sets isRecording to true', () => {
      const id    = createTranscriptionShape(editor, { isRecording: false })
      const shape = getShape(editor, id)

      toggle(util, shape)

      expect(getShape(editor, id).props.isRecording).toBe(true)
    })

    it('STARTING — resets segments to an empty array', () => {
      const id = createTranscriptionShape(editor, {
        isRecording: false,
        segments: [makeSegment('old transcript'), makeSegment('more old text')],
      })

      toggle(util, getShape(editor, id))

      expect(getShape(editor, id).props.segments).toHaveLength(0)
    })

    it('STARTING — clears currentSegment and lastProcessedSegment', () => {
      const id = createTranscriptionShape(editor, {
        isRecording: false,
        currentSegment: makeSegment('partial', { completed: false }),
        lastProcessedSegment: 'some prior segment',
      })

      toggle(util, getShape(editor, id))

      const props = getShape(editor, id).props
      expect(props.currentSegment).toBeUndefined()
      expect(props.lastProcessedSegment).toBeUndefined()
    })

    it('STARTING — calls TranscriptionManager.startTranscription with the shape id', () => {
      const id    = createTranscriptionShape(editor, { isRecording: false })
      const shape = getShape(editor, id)

      toggle(util, shape)

      expect(sm()).toHaveBeenCalledOnce()
      expect(sm()).toHaveBeenCalledWith(id)
    })

    it('STOPPING — sets isRecording to false', () => {
      const id    = createTranscriptionShape(editor, { isRecording: true })
      const shape = getShape(editor, id)

      toggle(util, shape)

      expect(getShape(editor, id).props.isRecording).toBe(false)
    })

    it('STOPPING — preserves existing segments (does not wipe them)', () => {
      const seg1 = makeSegment('Keep me.')
      const seg2 = makeSegment('Keep me too.')
      const id   = createTranscriptionShape(editor, {
        isRecording: true,
        segments: [seg1, seg2],
      })

      toggle(util, getShape(editor, id))

      const { segments } = getShape(editor, id).props
      expect(segments).toHaveLength(2)
      expect(segments[0].text).toBe('Keep me.')
      expect(segments[1].text).toBe('Keep me too.')
    })

    it('STOPPING — calls TranscriptionManager.stopTranscription', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })

      toggle(util, getShape(editor, id))

      expect(st()).toHaveBeenCalledOnce()
      expect(sm()).not.toHaveBeenCalled()
    })

    it('start-stop cycle leaves shape in consistent state', () => {
      const id = createTranscriptionShape(editor, { isRecording: false })

      toggle(util, getShape(editor, id))
      expect(getShape(editor, id).props.isRecording).toBe(true)

      util.updateText(id, 'Live lesson note.', true, { start: 0, end: 3 })

      toggle(util, getShape(editor, id))

      const props = getShape(editor, id).props
      expect(props.isRecording).toBe(false)
      expect(props.segments).toHaveLength(1)
      expect(props.segments[0].text).toBe('Live lesson note.')
    })
  })

  // ── Edge cases ─────────────────────────────────────────────────────────

  describe('updateText() — edge cases', () => {
    it('does nothing if the shape id does not exist in the store', () => {
      const ghostId = createShapeId('nonexistent')
      expect(() => util.updateText(ghostId, 'text', true, { start: 0, end: 1 })).not.toThrow()
    })

    it('handles empty string text without crashing', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })
      expect(() => util.updateText(id, '', true, { start: 0, end: 0 })).not.toThrow()
    })

    it('handles string start/end metadata (passed as already-formatted strings)', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })
      util.updateText(id, 'String timestamps.', true, { start: '1.234' as unknown as number, end: '5.678' as unknown as number })

      const seg = getShape(editor, id).props.segments[0]
      expect(typeof seg.start).toBe('string')
      expect(typeof seg.end).toBe('string')
    })

    it('handles missing metadata gracefully, falling back to "0.000"', () => {
      const id = createTranscriptionShape(editor, { isRecording: true })
      util.updateText(id, 'No timestamps.', true, undefined as unknown as { start: number; end: number })

      const seg = getShape(editor, id).props.segments[0]
      expect(seg.start).toBe('0.000')
      expect(seg.end).toBe('0.000')
    })
  })
})
