import 'dart:async';
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:marianum_mobile/api/api_error.dart';
import 'package:marianum_mobile/api/errors/auth_exception.dart';
import 'package:marianum_mobile/api/errors/error_mapper.dart';
import 'package:marianum_mobile/api/errors/network_exception.dart';
import 'package:marianum_mobile/api/errors/not_found_exception.dart';
import 'package:marianum_mobile/api/errors/parse_exception.dart';
import 'package:nextcloud/nextcloud.dart';

void main() {
  group('errorToUserMessage', () {
    test('null falls back to the default message', () {
      expect(errorToUserMessage(null), contains('Etwas ist schiefgelaufen'));
    });

    test('AppException returns its own userMessage', () {
      final exception = AuthException.unauthorized();
      expect(errorToUserMessage(exception), exception.userMessage);
    });

    test('SocketException maps to NetworkException message', () {
      expect(
        errorToUserMessage(const SocketException('boom')),
        const NetworkException().userMessage,
      );
    });

    test(
      'TimeoutException maps to the timeout-specific NetworkException message',
      () {
        expect(
          errorToUserMessage(TimeoutException('slow')),
          NetworkException.timeout().userMessage,
        );
      },
    );

    test('http.ClientException maps to NetworkException message', () {
      expect(
        errorToUserMessage(http.ClientException('failed')),
        const NetworkException().userMessage,
      );
    });

    test('HandshakeException maps to a TLS-specific message', () {
      final message = errorToUserMessage(const HandshakeException('bad cert'));
      expect(message, contains('sichere Verbindung'));
      expect(message, contains('Geräte-Uhrzeit'));
    });

    test('FormatException maps to ParseException message', () {
      expect(
        errorToUserMessage(const FormatException('bad json')),
        const ParseException().userMessage,
      );
    });

    test('ApiError surfaces only the first line of its message', () {
      final err = ApiError('Boom\nGET https://example.com/foo');
      expect(errorToUserMessage(err), 'Boom');
    });

    test('ApiError with empty message falls back to default', () {
      final err = ApiError('');
      expect(errorToUserMessage(err), contains('Etwas ist schiefgelaufen'));
    });

    test('unknown error type falls back', () {
      expect(
        errorToUserMessage(StateError('weird')),
        contains('Etwas ist schiefgelaufen'),
      );
    });

    test('custom fallback overrides the default', () {
      expect(errorToUserMessage(null, fallback: 'meins'), 'meins');
    });

    test('DioException connectionTimeout maps to timeout NetworkException', () {
      final ex = DioException(
        requestOptions: RequestOptions(path: '/x'),
        type: DioExceptionType.connectionTimeout,
      );
      expect(errorToUserMessage(ex), NetworkException.timeout().userMessage);
    });

    test('DioException connectionError maps to NetworkException', () {
      final ex = DioException(
        requestOptions: RequestOptions(path: '/x'),
        type: DioExceptionType.connectionError,
      );
      expect(errorToUserMessage(ex), const NetworkException().userMessage);
    });

    test('DioException badResponse maps to the server error message', () {
      final ex = DioException(
        requestOptions: RequestOptions(path: '/x'),
        type: DioExceptionType.badResponse,
        response: Response(
          requestOptions: RequestOptions(path: '/x'),
          statusCode: 503,
        ),
      );
      // The status code lives in the technical details, not the message.
      expect(
        errorToUserMessage(ex),
        contains('konnte die Anfrage gerade nicht verarbeiten'),
      );
      expect(errorToTechnicalDetails(ex), contains('503'));
    });
  });

  group('DynamiteApiException mapping', () {
    test('500 maps to the server error message without the raw dump', () {
      const ex = DynamiteApiException(500, {'server': 'nginx'}, '');
      expect(
        errorToUserMessage(ex),
        contains('konnte die Anfrage gerade nicht verarbeiten'),
      );
      expect(errorToTechnicalDetails(ex), 'HTTP 500');
      expect(errorToTechnicalDetails(ex), isNot(contains('nginx')));
      expect(errorAllowsRetry(ex), isTrue);
    });

    test('5xx details include a trimmed body preview', () {
      const ex = DynamiteApiException(503, {}, '  Service\n  Unavailable  ');
      expect(errorToTechnicalDetails(ex), 'HTTP 503 body=Service Unavailable');
    });

    test('long bodies are capped in the details', () {
      final ex = DynamiteApiException(502, const {}, 'x' * 600);
      final details = errorToTechnicalDetails(ex)!;
      expect(details, startsWith('HTTP 502 body='));
      expect(details, endsWith('…'));
      expect(details.length, lessThan(600));
    });

    test('401 maps to the unauthorized AuthException', () {
      const ex = DynamiteApiException(401, {}, '');
      expect(
        errorToUserMessage(ex),
        AuthException.unauthorized().userMessage,
      );
      expect(errorAllowsRetry(ex), isFalse);
    });

    test('403 maps to the forbidden AuthException', () {
      const ex = DynamiteApiException(403, {}, '');
      expect(errorToUserMessage(ex), AuthException.forbidden().userMessage);
      expect(errorAllowsRetry(ex), isFalse);
    });

    test('404 maps to NotFoundException', () {
      const ex = DynamiteApiException(404, {}, '');
      expect(errorToUserMessage(ex), const NotFoundException().userMessage);
      expect(errorAllowsRetry(ex), isFalse);
    });

    test('429 maps to a rate-limit message that allows retry', () {
      const ex = DynamiteApiException(429, {}, '');
      expect(errorToUserMessage(ex), contains('Zu viele Anfragen'));
      expect(errorAllowsRetry(ex), isTrue);
    });
  });

  group('errorToTechnicalDetails', () {
    test('null returns null', () {
      expect(errorToTechnicalDetails(null), isNull);
    });

    test('AppException uses its technicalDetails when set', () {
      final ex = AuthException.unauthorized(technicalDetails: 'http 401, foo');
      expect(errorToTechnicalDetails(ex), 'http 401, foo');
    });

    test('AppException without details falls back to toString()', () {
      final ex = AuthException.unauthorized();
      expect(errorToTechnicalDetails(ex), ex.toString());
    });

    test('arbitrary object stringifies', () {
      expect(errorToTechnicalDetails(StateError('x')), contains('x'));
    });
  });

  group('errorAllowsRetry', () {
    test('null allows retry by default', () {
      expect(errorAllowsRetry(null), isTrue);
    });

    test('AuthException disallows retry (allowRetry=false)', () {
      expect(errorAllowsRetry(AuthException.unauthorized()), isFalse);
    });

    test('NetworkException allows retry (allowRetry=true)', () {
      expect(errorAllowsRetry(const NetworkException()), isTrue);
    });

    test('non-AppException allows retry by default', () {
      expect(errorAllowsRetry(StateError('x')), isTrue);
    });
  });
}
