// Regression guard for Bug 1 ("字迹写完才出现"): the PDF editor's per-page overlay
// painter must read the in-progress stroke from a ValueListenable at PAINT time,
// so a mid-stroke update repaints the live ink WITHOUT re-running
// pageOverlaysBuilder (which only re-runs on setState). The old design captured
// the live stroke as a build-time snapshot, so _bumpOverlay repainted the
// painter but it still drew the stale (null) snapshot — the stroke only appeared
// on pointer-up.
//
// These tests pump the REAL _PageOverlayPainter (via LiveStrokeOverlayHarness)
// and assert (1) paint() reflects the CURRENT notifier value, and (2) updating
// the notifier alone drives a CustomPaint repaint with no widget rebuild.

import 'dart:ui' as ui;

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

import 'package:badnote/editor/canvas/pen_editor_screen.dart';
import 'package:badnote/editor/canvas/pen_stroke.dart';
import 'package:badnote/editor/engine/brush.dart';

PenStroke _stroke() => PenStroke(
      points: const [
        PenPoint(0.2, 0.2, 0.8),
        PenPoint(0.5, 0.5, 0.8),
        PenPoint(0.8, 0.8, 0.8),
      ],
      color: 0xFF000000,
      width: 0.01,
      kind: PenStrokeKind.pen,
      brush: BrushKind.fountainPen,
    );

/// Records the number of drawPath calls a painter issues for the given size.
int _drawPathCount(CustomPainter painter, Size size) {
  final recorder = ui.PictureRecorder();
  final canvas = _CountingCanvas(Canvas(recorder));
  painter.paint(canvas, size);
  recorder.endRecording().dispose();
  return canvas.drawPathCount;
}

void main() {
  const size = Size(100, 100);

  test('paint() reads the CURRENT live stroke from the notifier at paint time',
      () {
    final harness = LiveStrokeOverlayHarness(pageIndex: 0);
    addTearDown(harness.dispose);
    final painter = harness.buildPainter();

    // Idle: nothing to draw.
    expect(_drawPathCount(painter, size), 0);

    // Mid-stroke: pushing a live stroke into the notifier must make the SAME
    // painter instance draw it on the next paint (no reconstruction).
    harness.setLiveStroke(0, _stroke());
    expect(_drawPathCount(painter, size), 1,
        reason: 'painter must read the live stroke at paint time, not a '
            'build-time snapshot');

    // Cleared: back to nothing.
    harness.setLiveStroke(0, null);
    expect(_drawPathCount(painter, size), 0);
  });

  test('a live stroke on a DIFFERENT page is not painted here', () {
    final harness = LiveStrokeOverlayHarness(pageIndex: 0);
    addTearDown(harness.dispose);
    final painter = harness.buildPainter();

    harness.setLiveStroke(3, _stroke()); // belongs to page 3, not page 0
    expect(_drawPathCount(painter, size), 0);
  });

  testWidgets('updating the live-stroke notifier repaints without a rebuild',
      (tester) async {
    final harness = LiveStrokeOverlayHarness(pageIndex: 0);
    addTearDown(harness.dispose);

    var builds = 0;
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Builder(builder: (context) {
          builds++;
          return CustomPaint(
            painter: harness.buildPainter(),
            size: size,
          );
        }),
      ),
    );
    expect(builds, 1);

    // Pushing a live stroke must trigger a repaint of the CustomPaint via its
    // merged repaint Listenable — WITHOUT rebuilding the widget tree (which is
    // exactly the path _bumpOverlay/setState would NOT cover mid-stroke).
    harness.setLiveStroke(0, _stroke());
    await tester.pump();
    expect(builds, 1, reason: 'no widget rebuild should be needed');

    // And the painter now draws the live ink.
    expect(_drawPathCount(harness.buildPainter(), size), 1);
  });
}

/// A [Canvas] proxy that counts drawPath calls.
class _CountingCanvas implements Canvas {
  _CountingCanvas(this._inner);

  final Canvas _inner;
  int drawPathCount = 0;

  @override
  void drawPath(ui.Path path, ui.Paint paint) {
    drawPathCount++;
    _inner.drawPath(path, paint);
  }

  @override
  void noSuchMethod(Invocation invocation) =>
      _forward(invocation);

  dynamic _forward(Invocation i) => null;
}
