// test/brush_opacity_test.dart
//
// Pins brush OPACITY + BLEND compositing (closes TODO(brush-opacity)) at the
// geometry/paint-config level (NOT pixel snapshots), per the spec
// (docs/research/pen-brush-spec.md §3/§4):
//   (a) resolveStrokeOpacity: fountain solid (1.0), highlighter flat (<1),
//       ballpoint = 0.55 + 0.45·pressureAvg, pencil = 0.35 + 0.55·pressureAvg;
//   (b) the resolved Paint's color alpha reflects profile.opacity multiplied
//       into the stroke color's existing alpha (and pressure-tied for
//       ballpoint/pencil);
//   (c) highlighter composites with BlendMode.multiply; others BlendMode.srcOver;
//   (d) BOTH render paths' shared paint helpers agree (PenStroke path:
//       paintForStroke; EditorStroke path: paintForEditorStroke).

import 'dart:ui' show BlendMode;

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

import 'package:badnote/editor/canvas/ink_painters.dart' show paintForStroke;
import 'package:badnote/editor/canvas/pen_stroke.dart';
import 'package:badnote/editor/engine/brush.dart';
import 'package:badnote/editor/engine/stroke_geometry.dart'
    show paintForEditorStroke;
import 'package:badnote/editor/engine/stroke_model.dart';

int _alpha(int argb) => (argb >> 24) & 0xFF;

PenStroke _pen(BrushKind brush, int color, List<double> pressures) => PenStroke(
      points: [
        for (final p in pressures) PenPoint(0.1, 0.1, p),
      ],
      color: color,
      width: 0.006,
      kind: brush == BrushKind.highlighter
          ? PenStrokeKind.highlighter
          : PenStrokeKind.pen,
      brush: brush,
    );

EditorStroke _editor(BrushKind brush, int color, List<double> pressures) =>
    EditorStroke.create(
      points: [
        for (final p in pressures) EditorPoint(x: 0.1, y: 0.1, pressure: p),
      ],
      color: color,
      tool: brush == BrushKind.highlighter
          ? EditorTool.highlighter
          : EditorTool.pen,
      brush: brush,
    );

void main() {
  group('(a) resolveStrokeOpacity per brush (spec §3/§4)', () {
    test('fountain pen is solid (1.0) regardless of pressure', () {
      final b = brushProfileFor(BrushKind.fountainPen);
      expect(resolveStrokeOpacity(b, pressureAvg: 0.0), 1.0);
      expect(resolveStrokeOpacity(b, pressureAvg: 1.0), 1.0);
    });

    test('highlighter uses its flat profile opacity (0.35), not pressure', () {
      final b = brushProfileFor(BrushKind.highlighter);
      expect(resolveStrokeOpacity(b, pressureAvg: 0.0), b.opacity);
      expect(resolveStrokeOpacity(b, pressureAvg: 1.0), b.opacity);
      expect(b.opacity, lessThan(1.0));
    });

    test('ballpoint opacity = 0.55 + 0.45·pressureAvg (the "tell")', () {
      final b = brushProfileFor(BrushKind.ballpoint);
      expect(resolveStrokeOpacity(b, pressureAvg: 0.0), closeTo(0.55, 1e-9));
      expect(resolveStrokeOpacity(b, pressureAvg: 1.0), closeTo(1.0, 1e-9));
      expect(resolveStrokeOpacity(b, pressureAvg: 0.5), closeTo(0.775, 1e-9));
      // Pressure visibly modulates opacity (low < high).
      expect(resolveStrokeOpacity(b, pressureAvg: 0.2),
          lessThan(resolveStrokeOpacity(b, pressureAvg: 0.9)));
    });

    test('pencil opacity = 0.35 + 0.55·pressureAvg (lighter/translucent)', () {
      final b = brushProfileFor(BrushKind.pencil);
      expect(resolveStrokeOpacity(b, pressureAvg: 0.0), closeTo(0.35, 1e-9));
      expect(resolveStrokeOpacity(b, pressureAvg: 1.0), closeTo(0.90, 1e-9));
      expect(resolveStrokeOpacity(b, pressureAvg: 0.5), closeTo(0.625, 1e-9));
      // Pencil at any pressure is lighter than a solid fountain pen.
      expect(resolveStrokeOpacity(b, pressureAvg: 1.0), lessThan(1.0));
    });
  });

  group('(b) opacity multiplies into the color alpha', () {
    test('applyOpacityToArgb scales the existing alpha (keeps RGB)', () {
      // Opaque black at 0.5 → half alpha; RGB untouched.
      expect(applyOpacityToArgb(0xFF000000, 0.5), 0x80000000);
      // Already-translucent (0x80) highlighter color at 0.35 ⇒ composes
      // (no double-counting bug): 0x80 * 0.35 ≈ 45 (0x2D), not 0x80.
      final hi = applyOpacityToArgb(0x80FFEB3B, 0.35);
      expect(_alpha(hi), (0x80 * 0.35).round());
      expect(hi & 0x00FFFFFF, 0x00FFEB3B); // RGB preserved
    });

    test('fountain pen keeps full opacity in the resolved paint', () {
      final p = paintForStroke(_pen(BrushKind.fountainPen, 0xFF112233, [1.0]));
      expect(p.color.toARGB32(), 0xFF112233); // unchanged
      expect(p.blendMode, BlendMode.srcOver);
    });

    test('ballpoint resolved alpha tracks pressureAvg', () {
      final soft = paintForStroke(_pen(BrushKind.ballpoint, 0xFF000000, [0.0]));
      final hard = paintForStroke(_pen(BrushKind.ballpoint, 0xFF000000, [1.0]));
      // 0.55·255 ≈ 140; 1.0·255 = 255.
      expect(_alpha(soft.color.toARGB32()), (0xFF * 0.55).round());
      expect(_alpha(hard.color.toARGB32()), 0xFF);
      expect(_alpha(soft.color.toARGB32()),
          lessThan(_alpha(hard.color.toARGB32())));
    });

    test('pencil resolved alpha is lighter and tracks pressureAvg', () {
      final soft = paintForStroke(_pen(BrushKind.pencil, 0xFF000000, [0.0]));
      final hard = paintForStroke(_pen(BrushKind.pencil, 0xFF000000, [1.0]));
      expect(_alpha(soft.color.toARGB32()), (0xFF * 0.35).round());
      expect(_alpha(hard.color.toARGB32()), (0xFF * 0.90).round());
      // Pencil always lighter than fully-opaque fountain pen.
      expect(_alpha(hard.color.toARGB32()), lessThan(0xFF));
    });

    test('ballpoint differs from fountain pen via opacity at same pressure', () {
      final ball = paintForStroke(_pen(BrushKind.ballpoint, 0xFF000000, [0.3]));
      final fount =
          paintForStroke(_pen(BrushKind.fountainPen, 0xFF000000, [0.3]));
      expect(_alpha(ball.color.toARGB32()),
          lessThan(_alpha(fount.color.toARGB32())));
    });
  });

  group('(c) highlighter uses BlendMode.multiply, others srcOver', () {
    test('highlighter ⇒ multiply', () {
      final p =
          paintForStroke(_pen(BrushKind.highlighter, 0x80FFEB3B, [0.5, 0.5]));
      expect(p.blendMode, BlendMode.multiply);
      // 0.35 multiplied into the 0x80 capture alpha.
      expect(_alpha(p.color.toARGB32()), (0x80 * 0.35).round());
    });

    test('fountain/ballpoint/pencil ⇒ srcOver', () {
      for (final k in const [
        BrushKind.fountainPen,
        BrushKind.ballpoint,
        BrushKind.pencil,
      ]) {
        expect(paintForStroke(_pen(k, 0xFF000000, [0.5])).blendMode,
            BlendMode.srcOver,
            reason: '$k should not multiply');
      }
    });
  });

  group('(d) both render paths composite identically', () {
    test('EditorStroke path matches PenStroke path (alpha + blend)', () {
      for (final k in BrushKind.values) {
        final color = k == BrushKind.highlighter ? 0x80FFEB3B : 0xFF102030;
        const pressures = [0.2, 0.8];
        final penPaint = paintForStroke(_pen(k, color, pressures));
        final editorPaint = paintForEditorStroke(_editor(k, color, pressures));
        expect(editorPaint.color.toARGB32(), penPaint.color.toARGB32(),
            reason: '$k color mismatch across render paths');
        expect(editorPaint.blendMode, penPaint.blendMode,
            reason: '$k blend mismatch across render paths');
      }
    });
  });
}
