import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:recording_tool/features/recording/model/model_recording_session.dart';
import 'package:recording_tool/features/recording/platform/recording_channel_names.dart';
import 'package:recording_tool/features/recording/platform/recording_platform.dart';
import 'package:recording_tool/features/recording/view-model/view_model_recording.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  const defaultClipboardTitle = '';
  const validClipboardText =
      '{"title":"王东方 丨李想 空中格斗赛","startTimestamp":1717334400,"endTimestamp":1717334400,"filename":"选手名称_选手ID_赛事名称_赛项","address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}';

  Future<void> setClipboardText(String? text) async {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(SystemChannels.platform, (call) async {
          if (call.method == 'Clipboard.getData') {
            return text == null ? null : <String, dynamic>{'text': text};
          }
          return null;
        });
  }

  tearDown(() {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(SystemChannels.platform, null);
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(
          const MethodChannel(RecordingChannelNames.method),
          null,
        );
  });

  group('RecordingViewModel', () {
    test('initializes with default clipboard and session state', () {
      final container = ProviderContainer();
      addTearDown(container.dispose);

      final model = container.read(recordingViewModelProvider);
      expect(model.hasValidClipboardInfo, isFalse);
      expect(model.clipboardRecordingModel.title, defaultClipboardTitle);
      expect(model.session.isPreviewReady, isFalse);
      expect(model.session.isRecording, isFalse);
      expect(model.session.zoomRatio, 1.0);
      expect(model.session.minZoomRatio, 1.0);
      expect(model.session.maxZoomRatio, 3.0);
    });
  });

  group('RecordingViewModel.setZoomRatio', () {
    test('updates zoom ratio from native response', () async {
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(
            const MethodChannel(RecordingChannelNames.method),
            (call) async {
              expect(call.method, 'setZoomRatio');
              expect(call.arguments, <String, dynamic>{'zoomRatio': 2.0});
              return <String, dynamic>{
                'zoomRatio': 2.0,
                'minZoomRatio': 1.0,
                'maxZoomRatio': 3.0,
              };
            },
          );
      final container = ProviderContainer();
      addTearDown(container.dispose);

      await container.read(recordingViewModelProvider.notifier).setZoomRatio(2);

      final session = container.read(recordingViewModelProvider).session;
      expect(session.zoomRatio, 2.0);
      expect(session.minZoomRatio, 1.0);
      expect(session.maxZoomRatio, 3.0);
      expect(session.errorMessage, isNull);
    });

    test('passes 0.5x to native when camera capabilities allow it', () async {
      final calls = <MethodCall>[];
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(
            const MethodChannel(RecordingChannelNames.method),
            (call) async {
              calls.add(call);
              return <String, dynamic>{
                'zoomRatio': 0.5,
                'minZoomRatio': 0.5,
                'maxZoomRatio': 3.0,
              };
            },
          );
      final container = ProviderContainer();
      addTearDown(container.dispose);
      final notifier = container.read(recordingViewModelProvider.notifier);
      // ignore: invalid_use_of_protected_member
      notifier.state = container
          .read(recordingViewModelProvider)
          .copyWith(
            session: const RecordingSessionState(
              zoomRatio: 1.0,
              minZoomRatio: 0.5,
              maxZoomRatio: 3.0,
            ),
          );

      await notifier.setZoomRatio(0.5);

      expect(calls.single.arguments, <String, dynamic>{'zoomRatio': 0.5});
      final session = container.read(recordingViewModelProvider).session;
      expect(session.zoomRatio, 0.5);
      expect(session.minZoomRatio, 0.5);
      expect(session.maxZoomRatio, 3.0);
    });

    test('passes 0.6x to native when camera capabilities allow it', () async {
      final calls = <MethodCall>[];
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(
            const MethodChannel(RecordingChannelNames.method),
            (call) async {
              calls.add(call);
              return <String, dynamic>{
                'zoomRatio': 0.6,
                'minZoomRatio': 0.6,
                'maxZoomRatio': 3.0,
              };
            },
          );
      final container = ProviderContainer();
      addTearDown(container.dispose);
      final notifier = container.read(recordingViewModelProvider.notifier);
      // ignore: invalid_use_of_protected_member
      notifier.state = container
          .read(recordingViewModelProvider)
          .copyWith(
            session: const RecordingSessionState(
              zoomRatio: 1.0,
              minZoomRatio: 0.6,
              maxZoomRatio: 3.0,
            ),
          );

      await notifier.setZoomRatio(0.6);

      expect(calls.single.arguments, <String, dynamic>{'zoomRatio': 0.6});
      final session = container.read(recordingViewModelProvider).session;
      expect(session.zoomRatio, 0.6);
      expect(session.minZoomRatio, 0.6);
      expect(session.maxZoomRatio, 3.0);
    });

    test('clamps requested zoom ratio before invoking native', () async {
      final calls = <MethodCall>[];
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(
            const MethodChannel(RecordingChannelNames.method),
            (call) async {
              calls.add(call);
              return <String, dynamic>{
                'zoomRatio': 1.0,
                'minZoomRatio': 1.0,
                'maxZoomRatio': 1.0,
              };
            },
          );
      final container = ProviderContainer();
      addTearDown(container.dispose);

      await container.read(recordingViewModelProvider.notifier).setZoomRatio(4);

      expect(calls.single.arguments, <String, dynamic>{'zoomRatio': 3.0});
      expect(container.read(recordingViewModelProvider).session.zoomRatio, 1.0);
    });

    test(
      'clamps 0.6x to 1x when camera capabilities do not allow it',
      () async {
        final calls = <MethodCall>[];
        TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
            .setMockMethodCallHandler(
              const MethodChannel(RecordingChannelNames.method),
              (call) async {
                calls.add(call);
                return <String, dynamic>{
                  'zoomRatio': 1.0,
                  'minZoomRatio': 1.0,
                  'maxZoomRatio': 3.0,
                };
              },
            );
        final container = ProviderContainer();
        addTearDown(container.dispose);

        await container
            .read(recordingViewModelProvider.notifier)
            .setZoomRatio(0.6);

        expect(calls.single.arguments, <String, dynamic>{'zoomRatio': 1.0});
        expect(
          container.read(recordingViewModelProvider).session.zoomRatio,
          1.0,
        );
      },
    );

    test(
      'keeps previous zoom ratio and stores error when native fails',
      () async {
        TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
            .setMockMethodCallHandler(
              const MethodChannel(RecordingChannelNames.method),
              (call) async {
                throw PlatformException(
                  code: 'ZOOM_FAILED',
                  message: 'Zoom is unavailable',
                );
              },
            );
        final container = ProviderContainer();
        addTearDown(container.dispose);

        await container
            .read(recordingViewModelProvider.notifier)
            .setZoomRatio(2);

        final session = container.read(recordingViewModelProvider).session;
        expect(session.zoomRatio, 1.0);
        expect(session.errorMessage, '切换镜头失败，请重试');
      },
    );

    test(
      'maps native zoom failure to user friendly lens switch message',
      () async {
        TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
            .setMockMethodCallHandler(
              const MethodChannel(RecordingChannelNames.method),
              (call) async {
                throw PlatformException(
                  code: 'ZOOM_FAILED',
                  message: 'Cannot switch physical camera while recording',
                );
              },
            );
        final container = ProviderContainer();
        addTearDown(container.dispose);

        await container
            .read(recordingViewModelProvider.notifier)
            .setZoomRatio(0.6);

        final session = container.read(recordingViewModelProvider).session;
        expect(session.zoomRatio, 1.0);
        expect(session.errorMessage, '切换镜头失败，请重试');
      },
    );

    test('sets switching lens while native zoom request is pending', () async {
      final completer = Completer<Map<String, dynamic>>();
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(
            const MethodChannel(RecordingChannelNames.method),
            (call) => completer.future,
          );
      final container = ProviderContainer();
      addTearDown(container.dispose);
      final notifier = container.read(recordingViewModelProvider.notifier);

      final future = notifier.setZoomRatio(2);
      await Future<void>.delayed(Duration.zero);

      expect(
        container.read(recordingViewModelProvider).session.isSwitchingLens,
        isTrue,
      );

      completer.complete(<String, dynamic>{
        'zoomRatio': 2.0,
        'minZoomRatio': 1.0,
        'maxZoomRatio': 3.0,
      });
      await future;

      expect(
        container.read(recordingViewModelProvider).session.isSwitchingLens,
        isFalse,
      );
    });
  });

  group('RecordingViewModel.stopRecording', () {
    test('does not call native stop while switching lens', () async {
      final calls = <MethodCall>[];
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(
            const MethodChannel(RecordingChannelNames.method),
            (call) async {
              calls.add(call);
              return <String, dynamic>{};
            },
          );
      final container = ProviderContainer();
      addTearDown(container.dispose);
      final notifier = container.read(recordingViewModelProvider.notifier);
      // ignore: invalid_use_of_protected_member
      notifier.state = container
          .read(recordingViewModelProvider)
          .copyWith(
            session: const RecordingSessionState(
              status: RecordingStatus(state: RecordingState.recording),
              isSwitchingLens: true,
            ),
          );

      await notifier.stopRecording();

      expect(calls, isEmpty);
    });

    test(
      'stores segment output paths when native save falls back to parts',
      () async {
        TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
            .setMockMethodCallHandler(
              const MethodChannel(RecordingChannelNames.method),
              (call) async {
                switch (call.method) {
                  case 'stopRecording':
                    return <String, dynamic>{
                      'outputPath': 'content://recordings/part1',
                      'status': <String, dynamic>{
                        'state': 'error',
                        'message': 'Merge failed',
                      },
                      'fileSaved': false,
                      'fileErrorMessage': 'Merge failed',
                      'segmentOutputPaths': <String>[
                        'content://recordings/part1',
                        'content://recordings/part2',
                      ],
                    };
                  case 'initializePreview':
                    return <String, dynamic>{'state': 'previewing'};
                  case 'getZoomCapabilities':
                    return <String, dynamic>{
                      'zoomRatio': 1.0,
                      'minZoomRatio': 1.0,
                      'maxZoomRatio': 3.0,
                    };
                }
                return <String, dynamic>{};
              },
            );
        final container = ProviderContainer();
        addTearDown(container.dispose);
        final notifier = container.read(recordingViewModelProvider.notifier);
        // ignore: invalid_use_of_protected_member
        notifier.state = container
            .read(recordingViewModelProvider)
            .copyWith(
              session: const RecordingSessionState(
                status: RecordingStatus(state: RecordingState.recording),
              ),
            );

        await notifier.stopRecording();

        final session = container.read(recordingViewModelProvider).session;
        expect(session.fileSaveFailed, isTrue);
        expect(session.segmentOutputPaths, <String>[
          'content://recordings/part1',
          'content://recordings/part2',
        ]);
      },
    );
  });

  group('recordingFileSavePermissionsForHost', () {
    test('does not request photo permission on iOS', () {
      final permissions = recordingFileSavePermissionsForHost(
        isIOS: true,
        isAndroid: false,
      );

      expect(permissions, isEmpty);
      expect(permissions, isNot(contains(Permission.photosAddOnly)));
      expect(permissions, isNot(contains(Permission.photos)));
    });

    test('requests storage permission on Android 9 and below', () {
      final permissions = recordingFileSavePermissionsForHost(
        isIOS: false,
        isAndroid: true,
        androidSdkInt: 28,
      );

      expect(permissions, <Permission>[Permission.storage]);
      expect(permissions, isNot(contains(Permission.videos)));
    });

    test('does not request file save permission on Android 10 and above', () {
      final permissions = recordingFileSavePermissionsForHost(
        isIOS: false,
        isAndroid: true,
        androidSdkInt: 29,
      );

      expect(permissions, isEmpty);
    });

    test('does not request file save permission on Android 13 and above', () {
      final permissions = recordingFileSavePermissionsForHost(
        isIOS: false,
        isAndroid: true,
        androidSdkInt: 33,
      );

      expect(permissions, isEmpty);
    });

    test('does not request file save permission when Android SDK is unknown', () {
      final permissions = recordingFileSavePermissionsForHost(
        isIOS: false,
        isAndroid: true,
      );

      expect(permissions, isEmpty);
    });
  });

  group('RecordingViewModel.getClipboardContent', () {
    test(
      'updates state when clipboard contains valid mini program JSON',
      () async {
        await setClipboardText(validClipboardText);
        final container = ProviderContainer();
        addTearDown(container.dispose);

        final result = await container
            .read(recordingViewModelProvider.notifier)
            .getClipboardContent();

        expect(result, ClipboardReadResult.success);
        final model = container.read(recordingViewModelProvider);
        expect(model.hasValidClipboardInfo, isTrue);
        expect(model.clipboardRecordingModel.title, '王东方 丨李想 空中格斗赛');
        expect(model.clipboardRecordingModel.startTimestamp, 1717334400);
        expect(model.clipboardRecordingModel.endTimestamp, 1717334400);
        expect(model.clipboardRecordingModel.address, '广州市番禺区·粤港澳大湾区青年人才双创小镇');
        expect(model.clipboardRecordingModel.filename, '选手名称_选手ID_赛事名称_赛项');
      },
    );

    test('returns empty when clipboard is empty', () async {
      await setClipboardText('');
      final container = ProviderContainer();
      addTearDown(container.dispose);

      final result = await container
          .read(recordingViewModelProvider.notifier)
          .getClipboardContent();

      expect(result, ClipboardReadResult.empty);
      final model = container.read(recordingViewModelProvider);
      expect(model.hasValidClipboardInfo, isFalse);
      expect(model.clipboardRecordingModel.title, defaultClipboardTitle);
    });

    test('returns invalid when clipboard is not JSON', () async {
      await setClipboardText('hello');
      final container = ProviderContainer();
      addTearDown(container.dispose);

      final result = await container
          .read(recordingViewModelProvider.notifier)
          .getClipboardContent();

      expect(result, ClipboardReadResult.invalid);
      expect(
        container
            .read(recordingViewModelProvider)
            .clipboardRecordingModel
            .title,
        defaultClipboardTitle,
      );
      expect(
        container.read(recordingViewModelProvider).hasValidClipboardInfo,
        isFalse,
      );
    });

    test('returns invalid when clipboard JSON is not an object', () async {
      await setClipboardText('[1,2,3]');
      final container = ProviderContainer();
      addTearDown(container.dispose);

      final result = await container
          .read(recordingViewModelProvider.notifier)
          .getClipboardContent();

      expect(result, ClipboardReadResult.invalid);
      expect(
        container
            .read(recordingViewModelProvider)
            .clipboardRecordingModel
            .title,
        defaultClipboardTitle,
      );
    });

    test(
      'returns invalid when clipboard JSON misses required address',
      () async {
        await setClipboardText('{"title":"王东方 丨李想 空中格斗赛"}');
        final container = ProviderContainer();
        addTearDown(container.dispose);

        final result = await container
            .read(recordingViewModelProvider.notifier)
            .getClipboardContent();

        expect(result, ClipboardReadResult.invalid);
        expect(
          container
              .read(recordingViewModelProvider)
              .clipboardRecordingModel
              .title,
          defaultClipboardTitle,
        );
      },
    );

    test('updates state when clipboard omits optional timestamps', () async {
      await setClipboardText(
        '{"title":"郑昌梦 丨黄伟依 空中格斗赛 小学组","address":"广东省汕头市番禺区青蓝街 111 号","filename":"郑昌梦_黄伟依_6月3日测试-1_空中格斗赛"}',
      );
      final container = ProviderContainer();
      addTearDown(container.dispose);

      final result = await container
          .read(recordingViewModelProvider.notifier)
          .getClipboardContent();

      expect(result, ClipboardReadResult.success);
      final model = container.read(recordingViewModelProvider);
      expect(model.hasValidClipboardInfo, isTrue);
      expect(model.clipboardRecordingModel.startTimestamp, isNull);
      expect(model.clipboardRecordingModel.endTimestamp, isNull);
      expect(model.clipboardRecordingModel.filename, '郑昌梦_黄伟依_6月3日测试-1_空中格斗赛');
    });

    test('returns invalid when clipboard JSON has wrong field type', () async {
      await setClipboardText(
        '{"title":"王东方 丨李想 空中格斗赛","startTimestamp":"1717334400","endTimestamp":1717334400,"address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}',
      );
      final container = ProviderContainer();
      addTearDown(container.dispose);

      final result = await container
          .read(recordingViewModelProvider.notifier)
          .getClipboardContent();

      expect(result, ClipboardReadResult.invalid);
      expect(
        container
            .read(recordingViewModelProvider)
            .clipboardRecordingModel
            .title,
        defaultClipboardTitle,
      );
    });

    test('returns invalid when title is blank', () async {
      await setClipboardText(
        '{"title":"   ","startTimestamp":1717334400,"endTimestamp":1717334400,"address":"广州市"}',
      );
      final container = ProviderContainer();
      addTearDown(container.dispose);

      final result = await container
          .read(recordingViewModelProvider.notifier)
          .getClipboardContent();

      expect(result, ClipboardReadResult.invalid);
      expect(
        container.read(recordingViewModelProvider).hasValidClipboardInfo,
        isFalse,
      );
    });
  });
}
