// test/editor_stroke_model_test.dart
//
// Pure unit tests for the engine foundation (NO DB, NO widgets):
//   (a) EditorStroke -> json -> EditorStroke round-trips losslessly,
//       including tilt/timestamp/pointerDeviceKind.
//   (b) EditorStroke <-> InkStroke round-trips losslessly.
//   (c) buildStrokeOutline returns a non-empty Path for a >=2-point stroke and
//       an empty Path for 0 points.

import 'dart:convert';
import 'dart:ui';

import 'package:badnote/editor/engine/stroke_geometry.dart';
import 'package:badnote/editor/engine/stroke_model.dart';
import 'package:badnote/models/ink_point.dart';
import 'package:badnote/models/ink_stroke.dart';
import 'package:badnote/models/pen_tool.dart';
import 'package:badnote/models/pointer_device_kind.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('EditorStroke JSON round-trip', () {
    test('round-trips losslessly incl. tilt/timestamp/pointerDeviceKind', () {
      final stroke = EditorStroke(
        id: 'stroke-1',
        points: const [
          EditorPoint(
            x: 0.1,
            y: 0.2,
            pressure: 0.75,
            tilt: 0.33,
            timestamp: 1234567,
            pointerDeviceKind: InputDeviceKind.stylus,
          ),
          EditorPoint(
            x: 0.4,
            y: 0.5,
            pressure: 0.5,
            tilt: 0.0,
            timestamp: 1234600,
            pointerDeviceKind: InputDeviceKind.invertedStylus,
          ),
        ],
        tool: EditorTool.highlighter,
        color: 0xFFAABBCC,
        width: 0.0123,
        filled: true,
        textContent: 'hello',
        fontSize: 18.0,
      );

      // Persisted as a JSON string (DB TEXT column); jsonEncode invokes nested
      // toJson, jsonDecode rebuilds the maps.
      final decoded = EditorStroke.fromJson(
        jsonDecode(jsonEncode(stroke.toJson())) as Map<String, dynamic>,
      );

      expect(decoded, stroke);
      // Spot-check the superset fields explicitly.
      expect(decoded.points.first.tilt, 0.33);
      expect(decoded.points.first.timestamp, 1234567);
      expect(
        decoded.points.first.pointerDeviceKind,
        InputDeviceKind.stylus,
      );
      expect(
        decoded.points.last.pointerDeviceKind,
        InputDeviceKind.invertedStylus,
      );
    });

    test('preserves null superset fields', () {
      final stroke = EditorStroke(
        id: 'stroke-null',
        points: const [
          EditorPoint(x: 0.0, y: 0.0),
          EditorPoint(x: 1.0, y: 1.0, pressure: 0.9),
        ],
      );

      final decoded = EditorStroke.fromJson(
        jsonDecode(jsonEncode(stroke.toJson())) as Map<String, dynamic>,
      );

      expect(decoded, stroke);
      expect(decoded.points.first.pressure, isNull);
      expect(decoded.points.first.tilt, isNull);
      expect(decoded.points.first.timestamp, isNull);
      expect(decoded.points.first.pointerDeviceKind, isNull);
    });
  });

  group('EditorStroke <-> InkStroke round-trip', () {
    test('InkStroke -> EditorStroke -> InkStroke is lossless', () {
      final ink = InkStroke(
        id: 'ink-1',
        points: const [
          InkPoint(
            x: 0.2,
            y: 0.3,
            pressure: 0.8,
            tilt: 0.1,
            timestamp: 999,
            pointerDeviceKind: InputDeviceKind.stylus,
          ),
          InkPoint(
            x: 0.6,
            y: 0.7,
            pressure: 0.4,
            tilt: 0.2,
            timestamp: 1050,
            pointerDeviceKind: InputDeviceKind.touch,
          ),
        ],
        tool: PenTool.highlighter,
        color: 0xFF112233,
        strokeWidth: 0.02,
        createdAt: DateTime.fromMillisecondsSinceEpoch(42),
        filled: true,
        textContent: 'note',
        fontSize: 22.0,
      );

      final editor = EditorStroke.fromInkStroke(ink);
      final back = editor.toInkStroke(createdAt: ink.createdAt);

      expect(back, ink);
    });

    test('EditorStroke (non-null fields) -> InkStroke -> EditorStroke', () {
      final editor = EditorStroke(
        id: 'ink-2',
        points: const [
          EditorPoint(
            x: 0.1,
            y: 0.1,
            pressure: 0.5,
            tilt: 0.0,
            timestamp: 7,
            pointerDeviceKind: InputDeviceKind.mouse,
          ),
        ],
        tool: EditorTool.eraser,
        color: 0xFF000000,
        width: 0.004,
      );

      final ink = editor.toInkStroke();
      final back = EditorStroke.fromInkStroke(ink);

      expect(back, editor);
      expect(ink.tool, PenTool.eraser);
    });
  });

  group('buildStrokeOutline', () {
    const pageSize = Size(800, 600);

    test('returns a non-empty Path for a >=2-point stroke', () {
      final stroke = EditorStroke(
        id: 's',
        points: const [
          EditorPoint(x: 0.1, y: 0.1, pressure: 0.6),
          EditorPoint(x: 0.5, y: 0.4, pressure: 0.6),
          EditorPoint(x: 0.8, y: 0.9, pressure: 0.6),
        ],
        width: 0.01,
      );

      final path = buildStrokeOutline(stroke, pageSize, isComplete: true);
      expect(path.getBounds().isEmpty, isFalse);
    });

    test('returns an empty Path for 0 points', () {
      final stroke = EditorStroke(id: 'empty', points: const []);
      final path = buildStrokeOutline(stroke, pageSize, isComplete: true);
      expect(path.getBounds().isEmpty, isTrue);
    });
  });
}
