import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/errors/ticker_content_unavailable_exception.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/get_ticker/get_ticker_response.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/get_ticker_nav/get_ticker_nav_response.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/get_ticker_page/get_ticker_page.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/get_ticker_page/get_ticker_page_response.dart';

void main() {
  group('TickerResponse.fromJson', () {
    test('available post carries the nested content object', () {
      final response = TickerResponse.fromJson({
        'schemaVersion': 1,
        'available': true,
        'hash': 'R123:uuid@2026-07-08T10:00:00',
        'publishedAt': '2026-07-08T10:00:00',
        'webUrl': '/ticker',
        'content': {
          'type': 'doc',
          'content': [
            {
              'type': 'paragraph',
              'content': [
                {'type': 'text', 'text': 'Hallo'},
              ],
            },
          ],
        },
      });

      expect(response.available, isTrue);
      expect(response.schemaVersion, 1);
      expect(response.webUrl, '/ticker');
      expect(response.content, isA<Map<String, dynamic>>());
      expect(response.content!['type'], 'doc');
    });

    test('unavailable post keeps content null and defaults safely', () {
      final response = TickerResponse.fromJson({
        'schemaVersion': 1,
        'available': false,
        'webUrl': '/ticker',
        'content': null,
      });

      expect(response.available, isFalse);
      expect(response.content, isNull);
      expect(response.hash, isNull);
    });
  });

  group('TickerNavResponse.fromJson', () {
    test('parses sections with per-kind fields', () {
      final nav = TickerNavResponse.fromJson({
        'schemaVersion': 1,
        'navHash': 'nav-abc',
        'sections': [
          {
            'title': 'Informationen',
            'pages': [
              {'title': 'Über uns', 'slug': 'ueber-uns', 'kind': 'CONTENT'},
              {
                'title': 'Webseite',
                'slug': 'web',
                'kind': 'REDIRECT',
                'externalUrl': 'https://marianum-fulda.de',
                'externalUrlNewTab': true,
              },
              {
                'title': 'Flyer',
                'slug': 'flyer',
                'kind': 'PROXIED_FILE',
                'fileUrl': 'ticker/pages/flyer/file',
              },
            ],
          },
        ],
      });

      expect(nav.navHash, 'nav-abc');
      expect(nav.sections, hasLength(1));
      final pages = nav.sections.single.pages;
      expect(pages.map((p) => p.kind), ['CONTENT', 'REDIRECT', 'PROXIED_FILE']);
      expect(pages[1].externalUrl, 'https://marianum-fulda.de');
      expect(pages[1].externalUrlNewTab, isTrue);
      expect(pages[2].fileUrl, 'ticker/pages/flyer/file');
    });

    test('missing sections default to empty', () {
      final nav = TickerNavResponse.fromJson({
        'schemaVersion': 1,
        'navHash': '',
      });
      expect(nav.sections, isEmpty);
    });
  });

  group('TickerPageResponse.fromJson', () {
    test('CONTENT page exposes the nested document', () {
      final page = TickerPageResponse.fromJson({
        'schemaVersion': 1,
        'slug': 'ueber-uns',
        'title': 'Über uns',
        'kind': 'CONTENT',
        'content': {'type': 'doc', 'content': []},
      });
      expect(page.kind, TickerPageKind.content);
      expect(page.content, isNotNull);
    });

    test('REDIRECT page exposes the external URL', () {
      final page = TickerPageResponse.fromJson({
        'schemaVersion': 1,
        'kind': 'REDIRECT',
        'externalUrl': 'https://marianum-fulda.de',
      });
      expect(page.kind, TickerPageKind.redirect);
      expect(page.externalUrl, 'https://marianum-fulda.de');
    });

    test('PROXIED_FILE page exposes file metadata', () {
      final page = TickerPageResponse.fromJson({
        'schemaVersion': 1,
        'kind': 'PROXIED_FILE',
        'fileUrl': 'ticker/pages/flyer/file',
        'contentType': 'application/pdf',
        'filename': 'flyer.pdf',
      });
      expect(page.kind, TickerPageKind.proxiedFile);
      expect(page.contentType, 'application/pdf');
      expect(page.filename, 'flyer.pdf');
    });
  });

  group('GetTickerPage error mapping', () {
    test('404 CONTENT_UNAVAILABLE maps to a typed exception with webUrl', () async {
      final requestOptions = RequestOptions(path: 'ticker/pages/foo');
      final dio = _ThrowingDio(
        DioException(
          requestOptions: requestOptions,
          type: DioExceptionType.badResponse,
          response: Response<dynamic>(
            requestOptions: requestOptions,
            statusCode: 404,
            data: {'error': 'CONTENT_UNAVAILABLE', 'webUrl': '/ticker/p/foo'},
          ),
        ),
      );

      expect(
        () => GetTickerPage('foo', dio: dio).run(),
        throwsA(
          isA<TickerContentUnavailableException>()
              .having((e) => e.webUrl, 'webUrl', '/ticker/p/foo')
              .having((e) => e.allowRetry, 'allowRetry', isFalse),
        ),
      );
    });

    test('other 404 (NOT_FOUND) does not become CONTENT_UNAVAILABLE', () async {
      final requestOptions = RequestOptions(path: 'ticker/pages/foo');
      final dio = _ThrowingDio(
        DioException(
          requestOptions: requestOptions,
          type: DioExceptionType.badResponse,
          response: Response<dynamic>(
            requestOptions: requestOptions,
            statusCode: 404,
            data: {'error': 'NOT_FOUND', 'webUrl': '/ticker/p/foo'},
          ),
        ),
      );

      expect(
        () => GetTickerPage('foo', dio: dio).run(),
        throwsA(isNot(isA<TickerContentUnavailableException>())),
      );
    });
  });
}

/// Minimal fake Dio whose `get` always fails with the given exception; every
/// other member is unused.
class _ThrowingDio implements Dio {
  final DioException error;

  _ThrowingDio(this.error);

  @override
  Future<Response<T>> get<T>(
    String path, {
    Object? data,
    Map<String, dynamic>? queryParameters,
    Options? options,
    CancelToken? cancelToken,
    ProgressCallback? onReceiveProgress,
  }) => Future<Response<T>>.error(error);

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