// test/editor_render_test.dart
//
// Unit tests for the P0 RENDER layer:
//   (a) StrokeStore: add/removeById/replaceAll/clear all bump revision.
//   (b) StaticInkPainter.shouldRepaint: FALSE when revision+pageSize unchanged,
//       TRUE when revision changes. (The key perf invariant.)
//
// Tests are intentionally pure-logic: no widget pump, no GPU, no image
// comparisons. StaticInkPainter.shouldRepaint only reads store.revision and
// pageSize so we can exercise it without a real ui.Picture or Canvas.

import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:badnote/editor/engine/stroke_model.dart';
import 'package:badnote/editor/engine/stroke_store.dart';
import 'package:badnote/editor/render/ink_picture_cache.dart';
import 'package:badnote/editor/render/live_ink_painter.dart';
import 'package:badnote/editor/render/static_ink_painter.dart';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

EditorStroke _stroke(String id) => EditorStroke.create(
      id: id,
      points: const [
        EditorPoint(x: 0.1, y: 0.1, pressure: 0.5),
        EditorPoint(x: 0.5, y: 0.5, pressure: 0.5),
      ],
    );

/// Builds a [StaticInkPainter] that can be interrogated via [shouldRepaint]
/// without ever calling [paint] (avoids needing a real Canvas / Picture).
StaticInkPainter _painter(StrokeStore store, Size pageSize) =>
    StaticInkPainter(
      hostId: 'test-host',
      store: store,
      pageSize: pageSize,
      cache: InkPictureCache(),
    );

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

void main() {
  const pageSize = Size(800.0, 600.0);

  // -------------------------------------------------------------------------
  group('StrokeStore revision', () {
    test('starts at 0', () {
      final store = StrokeStore();
      expect(store.revision, 0);
    });

    test('add bumps revision', () {
      final store = StrokeStore();
      store.add(_stroke('s1'));
      expect(store.revision, 1);
      store.add(_stroke('s2'));
      expect(store.revision, 2);
      expect(store.committed.length, 2);
    });

    test('removeById bumps revision when stroke is found', () {
      final store = StrokeStore()..add(_stroke('s1'));
      final revBefore = store.revision;
      store.removeById('s1');
      expect(store.revision, greaterThan(revBefore));
      expect(store.committed, isEmpty);
    });

    test('removeById does NOT bump revision when id is absent', () {
      final store = StrokeStore()..add(_stroke('s1'));
      final revBefore = store.revision;
      store.removeById('nonexistent');
      expect(store.revision, revBefore);
    });

    test('replaceAll bumps revision', () {
      final store = StrokeStore();
      store.replaceAll([_stroke('a'), _stroke('b')]);
      expect(store.revision, 1);
      expect(store.committed.length, 2);

      store.replaceAll([_stroke('c')]);
      expect(store.revision, 2);
      expect(store.committed.length, 1);
    });

    test('clear bumps revision', () {
      final store = StrokeStore()..add(_stroke('x'));
      final revBefore = store.revision;
      store.clear();
      expect(store.revision, greaterThan(revBefore));
      expect(store.committed, isEmpty);
    });

    test('committed returns unmodifiable list', () {
      final store = StrokeStore()..add(_stroke('s'));
      expect(() => store.committed.add(_stroke('bad')), throwsUnsupportedError);
    });
  });

  // -------------------------------------------------------------------------
  group('StaticInkPainter.shouldRepaint', () {
    test('returns false when revision and pageSize are unchanged', () {
      final store = StrokeStore()..add(_stroke('s1'));
      final p1 = _painter(store, pageSize);
      final p2 = _painter(store, pageSize);

      // Both painters wrap the same store at the same revision.
      expect(p2.shouldRepaint(p1), isFalse);
    });

    test('returns true when revision changes', () {
      final store = StrokeStore()..add(_stroke('s1'));
      final p1 = _painter(store, pageSize);

      // Mutate the store — revision bumps.
      store.add(_stroke('s2'));
      final p2 = _painter(store, pageSize);

      expect(p2.shouldRepaint(p1), isTrue);
    });

    test('returns true when only pageSize changes', () {
      final store = StrokeStore()..add(_stroke('s1'));
      final p1 = _painter(store, pageSize);
      final p2 = _painter(store, const Size(1024.0, 768.0));

      expect(p2.shouldRepaint(p1), isTrue);
    });

    test('returns false after replaceAll with same content (revision differs '
        'but separate store instances — uses store.revision not identity)', () {
      // This test confirms shouldRepaint uses the revision INTEGER, not object
      // identity, so a brand-new store at revision 0 == another at revision 0.
      final storeA = StrokeStore(); // revision 0
      final storeB = StrokeStore(); // revision 0
      final pA = _painter(storeA, pageSize);
      final pB = _painter(storeB, pageSize);
      expect(pB.shouldRepaint(pA), isFalse);
    });

    test('returns true when old painter had higher revision than new '
        '(regression: revision comparison is not directional guard)', () {
      final store = StrokeStore()
        ..add(_stroke('a'))
        ..add(_stroke('b')); // revision == 2
      final pOld = _painter(store, pageSize);

      store.clear(); // revision == 3
      final pNew = _painter(store, pageSize);

      // pNew.revision (3) != pOld.revision (2) → should repaint.
      expect(pNew.shouldRepaint(pOld), isTrue);
    });

    // thinning (PenConfig.pressureSensitivity) participates in the static layer
    // identity: a sensitivity change rebuilds the outline, so it MUST trigger a
    // repaint even when revision + pageSize are unchanged — otherwise the cached
    // Picture (built at the old thinning) would be replayed (stale ink).
    test('returns true when only thinning changes (no stale cached Picture)',
        () {
      final store = StrokeStore()..add(_stroke('s1'));
      final cache = InkPictureCache();
      final pThin = StaticInkPainter(
          hostId: 'h', store: store, pageSize: pageSize, cache: cache,
          thinning: 0.85);
      final pThick = StaticInkPainter(
          hostId: 'h', store: store, pageSize: pageSize, cache: cache,
          thinning: 0.2);
      expect(pThick.shouldRepaint(pThin), isTrue);
    });

    test('returns false when thinning is equal (revision+size unchanged)', () {
      final store = StrokeStore()..add(_stroke('s1'));
      final cache = InkPictureCache();
      StaticInkPainter mk() => StaticInkPainter(
          hostId: 'h', store: store, pageSize: pageSize, cache: cache,
          thinning: 0.6);
      expect(mk().shouldRepaint(mk()), isFalse);
    });
  });

  // -------------------------------------------------------------------------
  group('StaticInkPainter.paint (cache + thinning recipe)', () {
    test('paints committed strokes into a Picture without error', () {
      final store = StrokeStore()
        ..add(_stroke('a'))
        ..add(_stroke('b'));
      final painter = _painter(store, pageSize);

      final recorder = ui.PictureRecorder();
      painter.paint(Canvas(recorder), pageSize);
      final picture = recorder.endRecording();
      addTearDown(picture.dispose);
      expect(picture, isNotNull);
    });

    test('a thinning change builds a fresh Picture (cache key includes thinning)',
        () {
      final store = StrokeStore()..add(_stroke('a'));
      final cache = InkPictureCache();
      // Same store/revision/size, different thinning, shared cache.
      for (final t in [0.85, 0.2]) {
        final painter = StaticInkPainter(
            hostId: 'h', store: store, pageSize: pageSize, cache: cache,
            thinning: t);
        final recorder = ui.PictureRecorder();
        painter.paint(Canvas(recorder), pageSize);
        recorder.endRecording().dispose();
      }
      // If the key ignored thinning, the 2nd paint would replay the 1st's
      // Picture; the test simply asserts both paints complete (distinct keys,
      // no aliasing assertion error from drawing a disposed picture).
      expect(true, isTrue);
    });
  });

  // -------------------------------------------------------------------------
  group('LiveInkPainter.shouldRepaint', () {
    test('repaints when the live stroke identity changes', () {
      const a = LiveInkPainter(live: null, pageSize: pageSize);
      final b = LiveInkPainter(
          live: _stroke('live'), pageSize: pageSize);
      expect(b.shouldRepaint(a), isTrue);
    });

    test('repaints when thinning changes (width consistent with static layer)',
        () {
      final stroke = _stroke('live');
      final a = LiveInkPainter(live: stroke, pageSize: pageSize, thinning: 0.85);
      final b = LiveInkPainter(live: stroke, pageSize: pageSize, thinning: 0.2);
      expect(b.shouldRepaint(a), isTrue);
    });
  });
}
