import 'dart:async';
import 'dart:io';
import 'package:fixnum/fixnum.dart';
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/market.pb.dart';
import 'package:alt_client/src/integrations/socket/alt_socket_client.dart';
import 'package:alt_client/src/features/market/data/market_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('MarketRepository tests', () {
    late FakeWebSocket fakeWs;
    late AltSocketClient client;
    late MarketRepository repository;

    setUp(() {
      fakeWs = FakeWebSocket();
      client = AltSocketClient(
        fakeWs,
        heartbeatIntervalTime: 10,
        heartbeatWaitTime: 10,
      );
      repository = MarketRepository(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('listInstruments success flows parsed results', () async {
      final request = ListInstrumentsRequest()
        ..market = Market.MARKET_US
        ..provider = 'kis';

      final expectedResponse = ListInstrumentsResponse()
        ..instruments.add(
          Instrument()
            ..id = 'instrument-1'
            ..market = Market.MARKET_US
            ..symbol = 'AAPL',
        );

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

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              ListInstrumentsRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest = ListInstrumentsRequest.fromBuffer(
            sentPacket.data,
          );
          expect(sentRequest.market, equals(Market.MARKET_US));
          expect(sentRequest.provider, equals('kis'));
        },
        responseTypeName:
            ListInstrumentsResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final result = await future;
      expect(result, isA<AltApiSuccess<ListInstrumentsResponse>>());
      final successVal =
          (result as AltApiSuccess<ListInstrumentsResponse>).value;
      expect(successVal.instruments, hasLength(1));
      expect(successVal.instruments.first.id, equals('instrument-1'));
      expect(successVal.instruments.first.symbol, equals('AAPL'));
    });

    test('listBars success flows historical data', () async {
      final request = ListBarsRequest()
        ..instrumentId = 'instrument-2'
        ..timeframe = Timeframe.TIMEFRAME_DAILY
        ..fromUnixMs = Int64(1000)
        ..toUnixMs = Int64(2000);

      final expectedResponse = ListBarsResponse()
        ..bars.add(
          Bar()
            ..instrumentId = 'instrument-2'
            ..timeframe = Timeframe.TIMEFRAME_DAILY
            ..timestampUnixMs = Int64(1500)
            ..close = Price(amount: Decimal(value: '150.0')),
        );

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

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(ListBarsRequest.getDefault().info_.qualifiedMessageName),
          );
          final sentRequest = ListBarsRequest.fromBuffer(sentPacket.data);
          expect(sentRequest.instrumentId, equals('instrument-2'));
          expect(sentRequest.timeframe, equals(Timeframe.TIMEFRAME_DAILY));
          expect(sentRequest.fromUnixMs, equals(Int64(1000)));
          expect(sentRequest.toUnixMs, equals(Int64(2000)));
        },
        responseTypeName:
            ListBarsResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final result = await future;
      expect(result, isA<AltApiSuccess<ListBarsResponse>>());
      final successVal = (result as AltApiSuccess<ListBarsResponse>).value;
      expect(successVal.bars, hasLength(1));
      expect(successVal.bars.first.instrumentId, equals('instrument-2'));
      expect(successVal.bars.first.close.amount.value, equals('150.0'));
    });

    test('typed ErrorInfo maps correctly to AltApiFailure', () async {
      final request = ListInstrumentsRequest()
        ..market = Market.MARKET_KR
        ..provider = 'kis';

      final expectedResponse = ListInstrumentsResponse()
        ..error = ErrorInfo(
          code: 'PROVIDER_LIMIT_EXCEEDED',
          message: 'kis daily quota reached',
        );

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

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

      final result = await future;
      expect(result, isA<AltApiFailure<ListInstrumentsResponse>>());
      final failure = result as AltApiFailure<ListInstrumentsResponse>;
      expect(failure.error.code, equals('PROVIDER_LIMIT_EXCEEDED'));
      expect(failure.error.message, equals('kis daily quota reached'));
    });

    test('timeout maps correctly to AltTransportKind.timeout', () async {
      final request = ListInstrumentsRequest();

      // We do not feed any response packet so it will time out.
      final result = await repository.listInstruments(
        request,
        timeout: const Duration(milliseconds: 10),
      );

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

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

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

        // Feed a response with a type mismatch (e.g. HelloResponse instead of ListInstrumentsResponse)
        await roundTrip(
          trigger: () => future,
          onSent: (sentPacket) {},
          responseTypeName:
              HelloResponse.getDefault().info_.qualifiedMessageName,
          responseData: HelloResponse().writeToBuffer(),
        );

        final result = await future;
        expect(result, isA<AltTransportFailure<ListInstrumentsResponse>>());
        final failure = result as AltTransportFailure<ListInstrumentsResponse>;
        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.listInstruments(ListInstrumentsRequest());
      expect(result, isA<AltTransportFailure<ListInstrumentsResponse>>());
      final failure = result as AltTransportFailure<ListInstrumentsResponse>;
      expect(failure.kind, equals(AltTransportKind.disconnected));
      expect(failure.detail, contains('client is not alive'));
    });
  });
}
