import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

import 'package:superport_v2/core/network/api_client.dart';
import 'package:superport_v2/core/network/api_error.dart';

class _MockDio extends Mock implements Dio {}

class _MockApiErrorMapper extends Mock implements ApiErrorMapper {}

void main() {
  setUpAll(() {
    registerFallbackValue(RequestOptions(path: '/fallback'));
    registerFallbackValue(Options());
    registerFallbackValue(CancelToken());
  });

  group('ApiClient', () {
    late Dio dio;
    late ApiErrorMapper mapper;
    late ApiClient client;

    setUp(() {
      dio = _MockDio();
      mapper = _MockApiErrorMapper();
      client = ApiClient(dio: dio, errorMapper: mapper);
    });

    test('성공 응답을 반환한다', () async {
      final requestOptions = RequestOptions(path: '/vendors');
      final response = Response<Map<String, dynamic>>(
        data: {'data': []},
        statusCode: 200,
        requestOptions: requestOptions,
      );

      when(
        () => dio.get<Map<String, dynamic>>(
          any(),
          queryParameters: any(named: 'queryParameters'),
          options: any(named: 'options'),
          cancelToken: any(named: 'cancelToken'),
        ),
      ).thenAnswer((_) async => response);

      final result = await client.get<Map<String, dynamic>>('/vendors');

      expect(result, same(response));
      verify(
        () => dio.get<Map<String, dynamic>>(
          any(),
          queryParameters: any(named: 'queryParameters'),
          options: any(named: 'options'),
          cancelToken: any(named: 'cancelToken'),
        ),
      ).called(1);
    });

    test('DioException을 ApiException으로 매핑한다', () async {
      final requestOptions = RequestOptions(path: '/vendors');
      final dioError = DioException(
        requestOptions: requestOptions,
        type: DioExceptionType.badResponse,
        response: Response<dynamic>(
          requestOptions: requestOptions,
          statusCode: 400,
          data: {'message': '잘못된 요청입니다.'},
        ),
      );
      final expected = ApiException(
        code: ApiErrorCode.badRequest,
        message: '잘못된 요청입니다.',
        statusCode: 400,
        cause: dioError,
      );

      when(
        () => dio.get<Map<String, dynamic>>(
          any(),
          queryParameters: any(named: 'queryParameters'),
          options: any(named: 'options'),
          cancelToken: any(named: 'cancelToken'),
        ),
      ).thenThrow(dioError);

      when(() => mapper.map(dioError)).thenReturn(expected);

      expect(
        () => client.get<Map<String, dynamic>>('/vendors'),
        throwsA(same(expected)),
      );

      verify(() => mapper.map(dioError)).called(1);
    });

    group('unwrapAsMap', () {
      test('data 키가 있으면 내부 맵을 반환한다', () {
        final response = Response<dynamic>(
          requestOptions: RequestOptions(path: '/auth/login'),
          statusCode: 200,
          data: {
            'data': {'token': 'abc'},
          },
        );

        expect(client.unwrapAsMap(response), {'token': 'abc'});
      });

      test('data 키가 없으면 원본 맵을 반환한다', () {
        final response = Response<dynamic>(
          requestOptions: RequestOptions(path: '/menus'),
          statusCode: 200,
          data: {'items': []},
        );

        expect(client.unwrapAsMap(response), {'items': []});
      });

      test('본문이 null이면 빈 맵을 반환한다', () {
        final response = Response<dynamic>(
          requestOptions: RequestOptions(path: '/menus'),
          statusCode: 200,
        );

        expect(client.unwrapAsMap(response), isEmpty);
      });
    });

    group('unwrap', () {
      test('data 키가 있는 리스트를 반환한다', () {
        final response = Response<dynamic>(
          requestOptions: RequestOptions(path: '/reports'),
          statusCode: 200,
          data: {
            'data': [1, 2, 3],
          },
        );

        expect(client.unwrap(response), [1, 2, 3]);
      });

      test('data 키가 없으면 원본을 그대로 반환한다', () {
        final response = Response<dynamic>(
          requestOptions: RequestOptions(path: '/health'),
          statusCode: 200,
          data: {'status': 'ok'},
        );

        expect(client.unwrap(response), {'status': 'ok'});
      });
    });

    group('buildPath', () {
      test('선행 슬래시를 유지하며 세그먼트를 결합한다', () {
        final path = ApiClient.buildPath('/api/v1', ['approvals', 1, 'steps']);
        expect(path, '/api/v1/approvals/1/steps');
      });

      test('세그먼트의 중복 슬래시를 제거한다', () {
        final path = ApiClient.buildPath('/api/v1/', ['/approval/', '/submit']);
        expect(path, '/api/v1/approval/submit');
      });
    });

    group('buildQuery', () {
      test('페이지네이션/검색 파라미터를 정규화한다', () {
        final now = DateTime.utc(2025, 1, 5, 12, 30);
        final query = ApiClient.buildQuery(
          page: 2,
          pageSize: 50,
          q: ' 품번 ',
          sort: ' transaction_date ',
          order: ' DESC ',
          include: const ['steps', 'histories', 'steps'],
          updatedSince: now,
          filters: {
            'transaction_id': 10,
            'status': ' 진행 ',
            'keyword': ' ',
            'from': DateTime.utc(2025, 1, 1, 9),
            'ids': [' 1 ', null, '2'],
          },
        );

        expect(query['page'], 2);
        expect(query['page_size'], 50);
        expect(query['q'], '품번');
        expect(query['sort'], 'transaction_date');
        expect(query['order'], 'desc');
        expect(query['include'], 'steps,histories');
        expect(query['updated_since'], now.toUtc().toIso8601String());
        expect(query['transaction_id'], 10);
        expect(query['status'], '진행');
        expect(query.containsKey('keyword'), isFalse);
        expect(
          query['from'],
          DateTime.utc(2025, 1, 1, 9).toUtc().toIso8601String(),
        );
        expect(query['ids'], '1,2');
        expect(() => query['page'] = 1, throwsUnsupportedError);
      });
    });
  });
}
