// test/badnote_sidecar_test.dart
//
// Round-trips a BadnoteSidecar containing per-page strokes + highlights +
// bookmarks + scratch-links (each with its own scratchpad of InkStrokes),
// asserting the re-parsed model equals the original. Verifies the sidecar reuses
// the existing EditorStroke / InkStroke / ScratchLink / Bookmark JSON shapes and
// that the schema `version` field survives.

import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';

import 'package:badnote/editor/engine/stroke_model.dart';
import 'package:badnote/models/bookmark.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:badnote/models/scratch_link.dart';
import 'package:badnote/storage/badnote_sidecar.dart';

EditorStroke _editorStroke(String id, EditorTool tool) => EditorStroke(
      id: id,
      points: [
        const EditorPoint(x: 0.1, y: 0.2, pressure: 0.5, tilt: 0.1),
        EditorPoint(
          x: 0.3,
          y: 0.4,
          pressure: 0.9,
          tilt: 0.2,
          timestamp: 1234,
          pointerDeviceKind: InputDeviceKind.stylus,
        ),
      ],
      tool: tool,
      color: 0xFF112233,
      width: 0.005,
    );

InkStroke _inkStroke(String id) => InkStroke(
      id: id,
      points: [
        const InkPoint(
          x: 100.0,
          y: 200.0,
          pressure: 0.7,
          tilt: 0.3,
          timestamp: 99,
          pointerDeviceKind: InputDeviceKind.stylus,
        ),
        const InkPoint(x: 300.0, y: 400.0, timestamp: 100),
      ],
      tool: PenTool.pen,
      color: 0xFF445566,
      strokeWidth: 3.0,
      createdAt: DateTime.utc(2026, 6, 24, 10, 0, 0),
    );

BadnoteSidecar _fullSidecar() => BadnoteSidecar(
      sourceFile: 'Calculus Lecture 3.pdf',
      docType: 'pdf',
      pageCount: 42,
      rotation: 90,
      createdAt: DateTime.utc(2026, 6, 24, 10, 0, 0),
      updatedAt: DateTime.utc(2026, 6, 24, 10, 32, 11),
      strokes: {
        0: [_editorStroke('s0', EditorTool.pen)],
        3: [
          _editorStroke('s1', EditorTool.highlighter),
          _editorStroke('s2', EditorTool.pen),
        ],
      },
      highlights: {
        0: const [
          SidecarHighlight(l: 0.12, t: 0.20, r: 0.88, b: 0.235, color: 0xFFFFEB3B),
        ],
      },
      bookmarks: [
        Bookmark(
          id: 'bm1',
          documentId: 'doc1',
          pageNumber: 5,
          label: 'Proof',
          color: 0xFF2196F3,
          createdAt: DateTime.utc(2026, 6, 24, 9, 0, 0),
        ),
      ],
      scratchLinks: [
        SidecarScratchLink(
          link: const ScratchLink(
            id: 'anchor1',
            documentId: 'doc1',
            pageIndex: 7,
            nx: 0.83,
            ny: 0.41,
          ),
          scratchpad: SidecarScratchpad(
            canvasWidth: 5000,
            canvasHeight: 6000,
            strokes: [_inkStroke('ink1'), _inkStroke('ink2')],
          ),
        ),
      ],
    );

/// Serializes through `jsonEncode`/`jsonDecode` exactly as `SidecarStore`
/// persists/loads on disk (nested freezed objects only flatten via the encoder's
/// toEncodable hook, so an in-memory map round-trip would not).
BadnoteSidecar _roundTrip(BadnoteSidecar sidecar) => BadnoteSidecar.fromJson(
    jsonDecode(jsonEncode(sidecar.toJson())) as Map<String, dynamic>);

void main() {
  test('full round-trip preserves strokes/highlights/bookmarks/links', () {
    final original = _fullSidecar();
    final reparsed = _roundTrip(original);

    expect(reparsed.version, kBadnoteSidecarVersion);
    expect(reparsed.sourceFile, 'Calculus Lecture 3.pdf');
    expect(reparsed.docType, 'pdf');
    expect(reparsed.pageCount, 42);
    expect(reparsed.rotation, 90);
    expect(reparsed.createdAt, DateTime.utc(2026, 6, 24, 10, 0, 0));
    expect(reparsed.updatedAt, DateTime.utc(2026, 6, 24, 10, 32, 11));

    // Strokes (EditorStroke value equality via freezed).
    expect(reparsed.strokes.keys.toSet(), {0, 3});
    expect(reparsed.strokes[0], original.strokes[0]);
    expect(reparsed.strokes[3], original.strokes[3]);

    // Highlights.
    expect(reparsed.highlights[0], original.highlights[0]);

    // Bookmarks (Bookmark value equality via freezed).
    expect(reparsed.bookmarks, original.bookmarks);

    // Scratch links + embedded scratchpads.
    expect(reparsed.scratchLinks, original.scratchLinks);
    final sp = reparsed.scratchLinks.single.scratchpad;
    expect(sp.canvasWidth, 5000);
    expect(sp.canvasHeight, 6000);
    expect(sp.strokes, original.scratchLinks.single.scratchpad.strokes);
  });

  test('strokes JSON is byte-compatible with EditorStroke.toJson', () {
    final stroke = _editorStroke('s0', EditorTool.pen);
    final sidecar = BadnoteSidecar(strokes: {2: [stroke]});
    final json = sidecar.toJson();
    final pageList = (json['strokes'] as Map)['2'] as List;
    expect(pageList.single, stroke.toJson());
  });

  test('scratchpad strokes JSON is byte-compatible with InkStroke.toJson', () {
    final ink = _inkStroke('ink1');
    final scratchpad = SidecarScratchpad(strokes: [ink]);
    final json = scratchpad.toJson();
    expect((json['strokes'] as List).single, ink.toJson());
  });

  test('scratch link JSON includes ScratchLink fields plus scratchpad', () {
    const link = ScratchLink(
      id: 'a',
      documentId: 'd',
      pageIndex: 3,
      nx: 0.5,
      ny: 0.5,
    );
    final json = SidecarScratchLink(link: link).toJson();
    // Reuses ScratchLink.toJson keys verbatim.
    for (final key in link.toJson().keys) {
      expect(json.containsKey(key), isTrue, reason: 'missing key $key');
    }
    expect(json.containsKey('scratchpad'), isTrue);
  });

  test('empty sidecar round-trips to empty containers', () {
    final reparsed = _roundTrip(BadnoteSidecar());
    expect(reparsed.strokes, isEmpty);
    expect(reparsed.highlights, isEmpty);
    expect(reparsed.bookmarks, isEmpty);
    expect(reparsed.scratchLinks, isEmpty);
    expect(reparsed.version, kBadnoteSidecarVersion);
  });

  test('unknown fields are ignored (forward-compat)', () {
    final encoded = jsonEncode(BadnoteSidecar(
      strokes: {0: [_editorStroke('s0', EditorTool.pen)]},
    ).toJson());
    final json = jsonDecode(encoded) as Map<String, dynamic>;
    json['futureFieldWeDoNotKnow'] = {'anything': true};
    ((json['strokes'] as Map)['0'] as List)[0]['brush'] =
        'marker'; // future per-stroke field
    final reparsed = BadnoteSidecar.fromJson(json);
    expect(reparsed.strokes[0]!.single.id, 's0');
  });

  test('missing scratchpad defaults to 4000x4000 empty pad', () {
    final json = SidecarScratchLink(
      link: const ScratchLink(
        id: 'a',
        documentId: 'd',
        pageIndex: 0,
        nx: 0,
        ny: 0,
      ),
    ).toJson();
    json.remove('scratchpad');
    final reparsed = SidecarScratchLink.fromJson(json);
    expect(reparsed.scratchpad.canvasWidth, 4000.0);
    expect(reparsed.scratchpad.canvasHeight, 4000.0);
    expect(reparsed.scratchpad.strokes, isEmpty);
  });
}
