import 'dart:async';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:proto_socket/proto_socket.dart';

import 'package:alt_client/src/contracts/alt_api_result.dart';
import 'package:alt_client/src/generated/alt/v1/common.pb.dart';
import 'package:alt_client/src/generated/alt/v1/backtest.pb.dart';
import 'package:alt_client/src/integrations/socket/alt_socket_client.dart';
import 'package:alt_client/src/features/backtest/data/backtest_repository.dart';

class FakeWebSocket extends Stream<dynamic> implements WebSocket {
  final StreamController<dynamic> _controller = StreamController<dynamic>();
  final List<List<int>> sentBytes = [];
  bool isClosed = false;

  @override
  void add(dynamic data) {
    if (data is List<int>) {
      sentBytes.add(data);
    }
  }

  @override
  Future<void> close([int? code, String? reason]) async {
    isClosed = true;
    await _controller.close();
  }

  void feedFromServer(List<int> data) {
    _controller.add(data);
  }

  @override
  StreamSubscription<dynamic> listen(
    void Function(dynamic event)? onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) {
    return _controller.stream.listen(
      onData,
      onError: onError,
      onDone: onDone,
      cancelOnError: cancelOnError,
    );
  }

  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

void main() {
  group('BacktestRepository tests', () {
    late FakeWebSocket fakeWs;
    late AltSocketClient client;
    late BacktestRepository repository;

    setUp(() {
      fakeWs = FakeWebSocket();
      client = AltSocketClient(
        fakeWs,
        heartbeatIntervalTime: 10,
        heartbeatWaitTime: 10,
      );
      repository = BacktestRepository(client);
    });

    tearDown(() async {
      await client.close();
    });

    Future<PacketBase> roundTrip({
      required Future<void> Function() trigger,
      required void Function(PacketBase sentPacket) onSent,
      required String responseTypeName,
      required List<int> responseData,
    }) async {
      final sentCountBefore = fakeWs.sentBytes.length;
      unawaited(trigger());

      await Future<void>.delayed(Duration.zero);

      expect(fakeWs.sentBytes.length, greaterThan(sentCountBefore));
      final sentPacket = PacketBase.fromBuffer(fakeWs.sentBytes.last);
      onSent(sentPacket);

      final responsePacket = PacketBase()
        ..typeName = responseTypeName
        ..nonce = 200
        ..responseNonce = sentPacket.nonce
        ..data = responseData;
      fakeWs.feedFromServer(responsePacket.writeToBuffer());

      return sentPacket;
    }

    test('listBacktestRuns success flows', () async {
      final request = ListBacktestRunsRequest()
        ..status = BacktestRunStatus.BACKTEST_RUN_STATUS_RUNNING;
      final expectedResponse = ListBacktestRunsResponse()
        ..runs.add(
          BacktestRun()
            ..id = 'run-1'
            ..spec = (BacktestRunSpec()..strategyId = 'strat-1'),
        );

      final future = repository.listBacktestRuns(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              ListBacktestRunsRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest = ListBacktestRunsRequest.fromBuffer(
            sentPacket.data,
          );
          expect(
            sentRequest.status,
            equals(BacktestRunStatus.BACKTEST_RUN_STATUS_RUNNING),
          );
        },
        responseTypeName:
            ListBacktestRunsResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final result = await future;
      expect(result, isA<AltApiSuccess<ListBacktestRunsResponse>>());
      final val = (result as AltApiSuccess<ListBacktestRunsResponse>).value;
      expect(val.runs, hasLength(1));
      expect(val.runs.first.id, equals('run-1'));
    });

    test('getBacktestRunDetail success flows', () async {
      final request = GetBacktestRunDetailRequest()..runId = 'run-1';
      final expectedResponse = GetBacktestRunDetailResponse()
        ..run = (BacktestRun()
          ..id = 'run-1'
          ..spec = (BacktestRunSpec()..strategyId = 'strat-1'));

      final future = repository.getBacktestRunDetail(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              GetBacktestRunDetailRequest.getDefault()
                  .info_
                  .qualifiedMessageName,
            ),
          );
          final sentRequest = GetBacktestRunDetailRequest.fromBuffer(
            sentPacket.data,
          );
          expect(sentRequest.runId, equals('run-1'));
        },
        responseTypeName: GetBacktestRunDetailResponse.getDefault()
            .info_
            .qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final result = await future;
      expect(result, isA<AltApiSuccess<GetBacktestRunDetailResponse>>());
      final val = (result as AltApiSuccess<GetBacktestRunDetailResponse>).value;
      expect(val.run.id, equals('run-1'));
    });

    test('getBacktestResult success flows', () async {
      final request = GetBacktestResultRequest()..runId = 'run-1';
      final expectedResponse = GetBacktestResultResponse()
        ..result = (BacktestResult()
          ..runId = 'run-1'
          ..summary = (BacktestSummaryMetrics()..tradeCount = 42));

      final future = repository.getBacktestResult(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              GetBacktestResultRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest = GetBacktestResultRequest.fromBuffer(
            sentPacket.data,
          );
          expect(sentRequest.runId, equals('run-1'));
        },
        responseTypeName:
            GetBacktestResultResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final result = await future;
      expect(result, isA<AltApiSuccess<GetBacktestResultResponse>>());
      final val = (result as AltApiSuccess<GetBacktestResultResponse>).value;
      expect(val.result.runId, equals('run-1'));
      expect(val.result.summary.tradeCount, equals(42));
    });

    test('startBacktest success flows', () async {
      final request = StartBacktestRequest()
        ..spec = (BacktestRunSpec()
          ..strategyId = 'strat-1'
          ..market = Market.MARKET_US);
      final expectedResponse = StartBacktestResponse()
        ..run = (BacktestRun()..id = 'run-new');

      final future = repository.startBacktest(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              StartBacktestRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest = StartBacktestRequest.fromBuffer(sentPacket.data);
          expect(sentRequest.spec.strategyId, equals('strat-1'));
          expect(sentRequest.spec.market, equals(Market.MARKET_US));
        },
        responseTypeName:
            StartBacktestResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final result = await future;
      expect(result, isA<AltApiSuccess<StartBacktestResponse>>());
      final val = (result as AltApiSuccess<StartBacktestResponse>).value;
      expect(val.run.id, equals('run-new'));
    });

    test('ErrorInfo maps correctly to AltApiFailure across calls', () async {
      final request = StartBacktestRequest()
        ..spec = (BacktestRunSpec()..strategyId = 'invalid-strat');
      final expectedResponse = StartBacktestResponse()
        ..error = ErrorInfo(
          code: 'STRATEGY_NOT_FOUND',
          message: 'no strategy with id invalid-strat',
        );

      final future = repository.startBacktest(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {},
        responseTypeName:
            StartBacktestResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final result = await future;
      expect(result, isA<AltApiFailure<StartBacktestResponse>>());
      final failure = result as AltApiFailure<StartBacktestResponse>;
      expect(failure.error.code, equals('STRATEGY_NOT_FOUND'));
      expect(
        failure.error.message,
        equals('no strategy with id invalid-strat'),
      );
    });

    test('timeout maps correctly to AltTransportKind.timeout', () async {
      final request = StartBacktestRequest();
      final result = await repository.startBacktest(
        request,
        timeout: const Duration(milliseconds: 10),
      );

      expect(result, isA<AltTransportFailure<StartBacktestResponse>>());
      final failure = result as AltTransportFailure<StartBacktestResponse>;
      expect(failure.kind, equals(AltTransportKind.timeout));
    });

    test(
      'type mismatch maps correctly to AltTransportKind.decodeFailure',
      () async {
        final request = StartBacktestRequest();

        final future = repository.startBacktest(
          request,
          timeout: const Duration(seconds: 1),
        );

        await roundTrip(
          trigger: () => future,
          onSent: (sentPacket) {},
          responseTypeName:
              HelloResponse.getDefault().info_.qualifiedMessageName,
          responseData: HelloResponse().writeToBuffer(),
        );

        final result = await future;
        expect(result, isA<AltTransportFailure<StartBacktestResponse>>());
        final failure = result as AltTransportFailure<StartBacktestResponse>;
        expect(failure.kind, equals(AltTransportKind.decodeFailure));
        expect(failure.detail, contains('Bad state'));
      },
    );

    test('client not alive returns AltTransportKind.disconnected', () async {
      await client.close();

      final result = await repository.startBacktest(StartBacktestRequest());
      expect(result, isA<AltTransportFailure<StartBacktestResponse>>());
      final failure = result as AltTransportFailure<StartBacktestResponse>;
      expect(failure.kind, equals(AltTransportKind.disconnected));
      expect(failure.detail, contains('client is not alive'));
    });
  });
}
