// Unit tests for the `friendlyError` mapper. These check the
// duck-typed shape contract — `GrpcError` from package:grpc is
// expected to expose `.code` (int), `.message` (String?), and
// `.codeName` (String) — without taking grpc as a Studio
// dependency. The fakes here mimic that shape.

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fai_studio/data/friendly_error.dart';
import 'package:fai_studio/l10n/app_localizations.dart';

class _FakeGrpcError {
  final int code;
  final String? message;
  final String codeName;
  _FakeGrpcError(this.code, this.codeName, [this.message]);
}

Future<AppLocalizations> _loadL10n(Locale locale) async {
  return await AppLocalizations.delegate.load(locale);
}

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  setUpAll(() async {
    // Force loading the gen-l10n delegate before tests so each
    // call to `_loadL10n` is fast.
    await _loadL10n(const Locale('en'));
  });

  test('UNAVAILABLE maps to hub-not-reachable copy', () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(_FakeGrpcError(14, 'UNAVAILABLE', 'connection refused'), l);
    expect(r.headline, l.errUnavailable);
    expect(r.hint, l.errUnavailableHint);
    expect(r.detail, 'connection refused');
  });

  test('FAILED_PRECONDITION carries module detail in body', () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(
      _FakeGrpcError(9, 'FAILED_PRECONDITION',
          'install error: no store entry for system.approval'),
      l,
    );
    expect(r.headline, l.errFailedPrecondition);
    expect(r.hint, l.errFailedPreconditionHint);
    expect(r.detail, contains('system.approval'));
  });

  test('NOT_FOUND has a recovery hint', () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(_FakeGrpcError(5, 'NOT_FOUND', 'no such module'), l);
    expect(r.headline, l.errNotFound);
    expect(r.hint, isNotNull);
  });

  test('Unknown code falls back to codeName: message', () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(_FakeGrpcError(99, 'CUSTOM_CODE', 'something'), l);
    expect(r.headline, contains('CUSTOM_CODE'));
    expect(r.headline, contains('something'));
    expect(r.hint, isNull);
  });

  test('Non-gRPC error renders toString as headline', () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(FormatException('bad url'), l);
    expect(r.headline, contains('bad url'));
  });

  test('Locale switches the headline language', () async {
    final lDe = await _loadL10n(const Locale('de'));
    final r =
        friendlyError(_FakeGrpcError(14, 'UNAVAILABLE', 'connection refused'), lDe);
    expect(r.headline, lDe.errUnavailable);
    final lEn = await _loadL10n(const Locale('en'));
    expect(lDe.errUnavailable, isNot(equals(lEn.errUnavailable)));
  });

  // --- Hub-pattern-specific detection ---
  // The mapper inspects the message DETAIL for hub patterns and
  // returns a more specific FriendlyError than the gRPC code's
  // default. These guards lock the matchers — a regression here
  // would silently fall back to the generic gRPC-code copy.

  test("approval rejection maps to specific headline", () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(
      _FakeGrpcError(
        7,
        'PERMISSION_DENIED',
        "step 'ap1' rejected by alice: needs more detail",
      ),
      l,
    );
    expect(r.headline, l.errApprovalRejected);
    expect(r.hint, l.errApprovalRejectedHint);
  });

  test('approval timeout maps to specific headline', () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(
      _FakeGrpcError(
        4,
        'DEADLINE_EXCEEDED',
        "step 'ap1' timed out waiting for approval after 600s",
      ),
      l,
    );
    expect(r.headline, l.errApprovalTimedOut);
  });

  test('output-too-large maps to specific headline', () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(
      _FakeGrpcError(
        8,
        'RESOURCE_EXHAUSTED',
        "step 'extract' produced 52428800 bytes of output, "
            "exceeding the 10 MB cap",
      ),
      l,
    );
    expect(r.headline, l.errOutputTooLarge);
  });

  test('host-service-not-declared maps to specific headline', () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(
      _FakeGrpcError(
        14,
        'UNAVAILABLE',
        "step 'summarise' requires service 'ollama' which is not declared "
            "in operator config",
      ),
      l,
    );
    expect(r.headline, l.errServiceUnavailableForStep);
  });

  test('missing value maps to specific headline', () async {
    final l = await _loadL10n(const Locale('en'));
    final r = friendlyError(
      _FakeGrpcError(
        3,
        'INVALID_ARGUMENT',
        r"step 'classify' references missing value '$inputs.text'",
      ),
      l,
    );
    expect(r.headline, l.errMissingValue);
  });
}

