// test/brush_test.dart
//
// Pins the data-driven brush model (lib/editor/engine/brush.dart) against the
// authoritative spec (docs/research/pen-brush-spec.md §1 + §4):
//   (a) each preset's KEY perfect_freehand params match the spec table;
//   (b) the quadratic pressure warp via PressureCurve(gamma:2) behaves (p=0→
//       floor, p=1→1, p=0.5→~0.25 within the floor, monotonic);
//   (c) the four presets are DISTINCT (thinning + gamma differ), so a brush is
//       not merely a width re-label.

import 'package:flutter_test/flutter_test.dart';

import 'package:badnote/editor/engine/brush.dart';
import 'package:badnote/editor/input/pressure_curve.dart';

void main() {
  group('(a) presets match the spec §4 tables', () {
    test('all four kinds have a preset', () {
      for (final k in BrushKind.values) {
        expect(kBrushPresets.containsKey(k), isTrue, reason: 'missing $k');
        expect(brushProfileFor(k).kind, k);
      }
    });

    test('fountain pen — Pow2 (p²), thinning 0.9, taper on, solid', () {
      final b = brushProfileFor(BrushKind.fountainPen);
      expect(b.pressureGamma, 2.0); // rnote Pow2 / quadratic
      expect(b.pfThinning, 0.9);
      expect(b.pfStreamline, 0.45);
      expect(b.pfSmoothing, 0.55);
      expect(b.simulatePressure, isFalse);
      expect(b.taper, isTrue);
      expect(b.capStart, isTrue);
      expect(b.capEnd, isTrue);
      expect(b.opacity, 1.0);
      expect(b.blendMultiply, isFalse);
    });

    test('ballpoint — linear, near-constant width (thinning 0.15)', () {
      final b = brushProfileFor(BrushKind.ballpoint);
      expect(b.pressureGamma, 1.0); // rnote Linear
      expect(b.pfThinning, 0.15);
      expect(b.pfStreamline, 0.55);
      expect(b.simulatePressure, isFalse);
      expect(b.taper, isFalse);
    });

    test('highlighter — flat width (thinning 0), square caps, multiply', () {
      final b = brushProfileFor(BrushKind.highlighter);
      expect(b.pressureGamma, 1.0);
      expect(b.pfThinning, 0.0); // constant width
      expect(b.pfStreamline, 0.5);
      expect(b.pfSmoothing, 0.4);
      expect(b.capStart, isFalse); // square ends
      expect(b.capEnd, isFalse);
      expect(b.blendMultiply, isTrue); // marker build-up (applied later)
      expect(b.opacity, lessThan(1.0));
    });

    test('pencil — Sqrt (√p), moderate thinning 0.5, scratchy streamline', () {
      final b = brushProfileFor(BrushKind.pencil);
      expect(b.pressureGamma, 0.5); // rnote Sqrt / √p
      expect(b.pfThinning, 0.5);
      expect(b.pfStreamline, 0.4);
      expect(b.taper, isFalse);
    });
  });

  group('(b) quadratic warp via PressureCurve(gamma:2)', () {
    test('p=0 → floor, p=1 → 1 (endpoints)', () {
      const c = PressureCurve(floor: 0.15, gamma: 2.0);
      expect(c.apply(0.0), closeTo(0.15, 1e-9));
      expect(c.apply(1.0), closeTo(1.0, 1e-9));
    });

    test('p=0.5 → ~0.25 mapped into the floored range', () {
      // shaped = p² = 0.25; floored: floor + (1-floor)*0.25.
      const floor = 0.15;
      const c = PressureCurve(floor: floor, gamma: 2.0);
      expect(c.apply(0.5), closeTo(floor + (1 - floor) * 0.25, 1e-9));
      // With a zero floor it is exactly the bare quadratic 0.25.
      expect(const PressureCurve(gamma: 2.0).apply(0.5), closeTo(0.25, 1e-9));
    });

    test('monotonic non-decreasing across [0,1]', () {
      const c = PressureCurve(floor: 0.15, gamma: 2.0);
      var prev = c.apply(0.0);
      for (var i = 1; i <= 20; i++) {
        final v = c.apply(i / 20);
        expect(v, greaterThanOrEqualTo(prev), reason: 'non-monotonic at $i/20');
        prev = v;
      }
    });

    test('quadratic stays below linear in the interior (steeper ramp)', () {
      // p² < p for 0<p<1 — the fountain-pen "thin at low pressure" feel.
      const quad = PressureCurve(gamma: 2.0);
      const lin = PressureCurve(gamma: 1.0);
      for (final p in const [0.2, 0.4, 0.6, 0.8]) {
        expect(quad.apply(p), lessThan(lin.apply(p)));
      }
    });

    test('brush gammas drive distinct warps at p=0.5', () {
      double warp(BrushKind k) =>
          PressureCurve(gamma: brushProfileFor(k).pressureGamma).apply(0.5);
      final fountain = warp(BrushKind.fountainPen); // 0.25
      final ballpoint = warp(BrushKind.ballpoint); // 0.5
      final pencil = warp(BrushKind.pencil); // √0.5 ≈ 0.707
      expect(fountain, lessThan(ballpoint));
      expect(ballpoint, lessThan(pencil));
    });
  });

  group('(c) presets are distinct (not a width re-label)', () {
    test('thinning differs across all four', () {
      final thinnings =
          BrushKind.values.map((k) => brushProfileFor(k).pfThinning).toSet();
      // fountain 0.9, ballpoint 0.15, highlighter 0.0, pencil 0.5 ⇒ 4 distinct.
      expect(thinnings.length, BrushKind.values.length);
    });

    test('pressure gamma differs (fountain p² vs pencil √p vs linear)', () {
      expect(brushProfileFor(BrushKind.fountainPen).pressureGamma,
          isNot(brushProfileFor(BrushKind.pencil).pressureGamma));
      expect(brushProfileFor(BrushKind.fountainPen).pressureGamma,
          isNot(brushProfileFor(BrushKind.ballpoint).pressureGamma));
    });

    test('caps/taper differ (fountain tapers, highlighter is square)', () {
      expect(brushProfileFor(BrushKind.fountainPen).taper, isTrue);
      expect(brushProfileFor(BrushKind.highlighter).capStart, isFalse);
      expect(brushProfileFor(BrushKind.ballpoint).taper, isFalse);
    });
  });
}
