import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:marianum_mobile/push/push_avatar.dart';

final Uint8List _fakeBytes = Uint8List.fromList([
  0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 1, 2, 3, //
]);

http.Response _imageResponse() => http.Response.bytes(
  _fakeBytes,
  200,
  headers: {'content-type': 'image/png'},
);

/// Renders a solid 40×20 PNG via dart:ui — a real decodable source image.
Future<Uint8List> _generatePng() async {
  final recorder = ui.PictureRecorder();
  final canvas = ui.Canvas(recorder);
  canvas.drawRect(
    const ui.Rect.fromLTWH(0, 0, 40, 20),
    ui.Paint()..color = const ui.Color(0xFF993333),
  );
  final image = await recorder.endRecording().toImage(40, 20);
  final data = await image.toByteData(format: ui.ImageByteFormat.png);
  return data!.buffer.asUint8List();
}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late Directory tempDir;

  setUp(() async {
    tempDir = await Directory.systemTemp.createTemp('push_avatar_test');
  });

  tearDown(() async {
    await tempDir.delete(recursive: true);
  });

  PushAvatarStore store({
    required Future<http.Response> Function(String chatToken) fetch,
    Duration timeout = const Duration(seconds: 4),
  }) => PushAvatarStore(
    cacheDirProvider: () async => tempDir,
    fetch: fetch,
    fetchTimeout: timeout,
  );

  group('maskAvatarCircular', () {
    test('output is a square PNG sized to the shorter edge', () async {
      final source = await _generatePng();
      final masked = await maskAvatarCircular(source);
      expect(masked, isNotNull);
      // PNG magic bytes.
      expect(masked!.sublist(0, 8), [
        0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, //
      ]);
      final codec = await ui.instantiateImageCodec(masked);
      final frame = await codec.getNextFrame();
      expect(frame.image.width, 20);
      expect(frame.image.height, 20);
    });

    test('is deterministic for identical input', () async {
      final source = await _generatePng();
      final a = await maskAvatarCircular(source);
      final b = await maskAvatarCircular(source);
      expect(a, isNotNull);
      expect(a, equals(b));
    });

    test('undecodable bytes yield null (caller falls back to raw)', () async {
      expect(await maskAvatarCircular(_fakeBytes), isNull);
    });
  });

  group('roomAvatarIcon', () {
    test('fetches, caches, and serves from cache afterwards', () async {
      var fetchCount = 0;
      final s = store(
        fetch: (_) async {
          fetchCount++;
          return _imageResponse();
        },
      );

      final first = await s.roomAvatarIcon('iconToken1');
      // Fake bytes are undecodable → mask falls back to the raw bytes.
      expect(first.icon, _fakeBytes);
      expect(first.late, isNull);
      expect(fetchCount, 1);

      final second = await s.roomAvatarIcon('iconToken1');
      expect(second.icon, _fakeBytes);
      expect(fetchCount, 1);
    });

    test('svg placeholder is rejected and not cached', () async {
      final s = store(
        fetch: (_) async => http.Response(
          '<svg xmlns="http://www.w3.org/2000/svg"/>',
          200,
          headers: {'content-type': 'image/svg+xml'},
        ),
      );
      final lookup = await s.roomAvatarIcon('iconToken2');
      expect(lookup.icon, isNull);
      expect(lookup.late, isNull);
      expect(File('${tempDir.path}/iconToken2').existsSync(), isFalse);
    });

    test('definitive fetch error yields no icon and no late future', () async {
      final s = store(fetch: (_) async => throw const SocketException('down'));
      final lookup = await s.roomAvatarIcon('iconToken3');
      expect(lookup.icon, isNull);
      expect(lookup.late, isNull);
    });

    test('fetch error falls back to a stale cache entry', () async {
      final file = File('${tempDir.path}/iconToken4');
      await file.writeAsBytes(_fakeBytes);
      await file.setLastModified(
        DateTime.now().subtract(const Duration(days: 20)),
      );

      final s = store(fetch: (_) async => throw const SocketException('down'));
      final lookup = await s.roomAvatarIcon('iconToken4');
      expect(lookup.icon, _fakeBytes);
    });

    test(
      'timeout returns a late future that delivers exactly one result',
      () async {
        final completer = Completer<http.Response>();
        var fetchCount = 0;
        final s = store(
          fetch: (_) {
            fetchCount++;
            return completer.future;
          },
          timeout: const Duration(milliseconds: 50),
        );

        final lookup = await s.roomAvatarIcon('iconToken5');
        expect(lookup.icon, isNull);
        expect(lookup.late, isNotNull);

        completer.complete(_imageResponse());
        final lateBytes = await lookup.late;
        expect(lateBytes, _fakeBytes);

        // The processed cache now serves directly — no second fetch.
        final second = await s.roomAvatarIcon('iconToken5');
        expect(second.icon, _fakeBytes);
        expect(second.late, isNull);
        expect(fetchCount, 1);
      },
    );

    test('non-200 responses yield null', () async {
      final s = store(fetch: (_) async => http.Response('gone', 404));
      final lookup = await s.roomAvatarIcon('iconToken6');
      expect(lookup.icon, isNull);
      expect(lookup.late, isNull);
    });
  });

  group('fileNameForToken / isFresh / looksLikeSvg', () {
    test('url-safe tokens are used verbatim', () {
      expect(PushAvatarStore.fileNameForToken('abc123_XY-z'), 'abc123_XY-z');
    });

    test('unsafe tokens are encoded deterministically and stay distinct', () {
      final a = PushAvatarStore.fileNameForToken('a/b');
      expect(a, PushAvatarStore.fileNameForToken('a/b'));
      expect(a, isNot(PushAvatarStore.fileNameForToken('a.b')));
      expect(RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(a), isTrue);
    });

    test('isFresh boundary', () {
      final now = DateTime(2026, 7, 5, 12);
      expect(
        PushAvatarStore.isFresh(now.subtract(const Duration(days: 13)), now),
        isTrue,
      );
      expect(
        PushAvatarStore.isFresh(
          now.subtract(const Duration(days: 14, hours: 1)),
          now,
        ),
        isFalse,
      );
    });

    test('svg detection', () {
      expect(
        PushAvatarStore.looksLikeSvg(
          Uint8List.fromList('  <svg xmlns="..."/>'.codeUnits),
        ),
        isTrue,
      );
      expect(PushAvatarStore.looksLikeSvg(_fakeBytes), isFalse);
    });
  });
}
