import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:userfront/core/services/auth_proxy_service.dart';
import 'package:userfront/core/services/auth_token_store.dart';

void main() {
  late _RecordingClient client;

  setUp(() {
    client = _RecordingClient();
    AuthTokenStore.clear();
    AuthProxyService.debugSetHttpClientFactoryForTesting(({
      bool withCredentials = false,
    }) {
      client.withCredentialsCalls.add(withCredentials);
      return client;
    });
  });

  tearDown(() {
    AuthProxyService.debugSetHttpClientFactoryForTesting(null);
    AuthTokenStore.clear();
  });

  group('AuthProxyService HTTP contract', () {
    test('getMe는 bearer token과 cookie mode를 구분한다', () async {
      client.enqueueJson({'id': 'user-1'});

      final result = await AuthProxyService.getMe(
        token: 'jwt-token',
        useCookie: false,
      );

      expect(result['id'], 'user-1');
      expect(client.withCredentialsCalls, [false]);
      expect(
        client.requests.single.headers['Authorization'],
        'Bearer jwt-token',
      );
      expect(client.closedCount, 1);
    });

    test('checkCookieSession은 credential client로 profile을 조회한다', () async {
      client.enqueueJson({'email': 'user@example.com'});

      final result = await AuthProxyService.checkCookieSession();

      expect(result['email'], 'user@example.com');
      expect(client.withCredentialsCalls, [true]);
      expect(client.requests.single.url.path, '/api/v1/user/me');
    });

    test('initEnchantedLink는 개발 환경에서 drySend와 codeOnly를 전송한다', () async {
      client.enqueueJson({'pendingRef': 'pending-1'});

      await AuthProxyService.initEnchantedLink(
        'user@example.com',
        method: 'email',
        codeOnly: true,
        drySend: true,
      );

      final body = client.lastJsonBody;
      expect(body['loginId'], 'user@example.com');
      expect(body['method'], 'email');
      expect(body['codeOnly'], isTrue);
      expect(body['drySend'], isTrue);
      expect(body['uri'], isA<String>());
    });

    test('poll 계열은 pending 상태 400 응답 body를 정상 결과로 반환한다', () async {
      client.enqueueJson({'status': 'pending'}, statusCode: 400);

      final result = await AuthProxyService.pollEnchantedLink('pending-1');

      expect(result['status'], 'pending');
      expect(client.lastJsonBody['pendingRef'], 'pending-1');
    });

    test('verifyLoginCode는 pendingRef가 있을 때만 payload에 포함한다', () async {
      client.enqueueJson({'ok': true});

      await AuthProxyService.verifyLoginCode(
        'user@example.com',
        '123456',
        pendingRef: 'pending-1',
        verifyOnly: true,
      );

      expect(client.lastJsonBody, {
        'loginId': 'user@example.com',
        'code': '123456',
        'verifyOnly': true,
        'pendingRef': 'pending-1',
      });
    });

    test('fetchCurrentSessionId는 현재 세션만 반환하고 없으면 null을 반환한다', () async {
      AuthTokenStore.setToken('jwt-token');
      client.enqueueJson({
        'items': [
          {'session_id': 'old-session', 'is_current': false},
          {'session_id': 'current-session', 'is_current': true},
        ],
      });
      client.enqueueJson({
        'items': [
          {'session_id': 'old-session', 'is_current': false},
        ],
      });

      final current = await AuthProxyService.fetchCurrentSessionId();
      final missing = await AuthProxyService.fetchCurrentSessionId();

      expect(current, 'current-session');
      expect(missing, isNull);
      expect(
        client.requests.first.headers['Authorization'],
        'Bearer jwt-token',
      );
    });

    test(
      'consent error는 code/message/details를 AuthProxyException으로 보존한다',
      () async {
        client.enqueueJson({
          'code': 'tenant_not_allowed',
          'error': 'tenant blocked',
          'details': {
            'allowed_tenants': ['gp'],
          },
        }, statusCode: 403);

        await expectLater(
          AuthProxyService.getConsentInfo('consent-1'),
          throwsA(
            isA<AuthProxyException>()
                .having(
                  (error) => error.errorCode,
                  'code',
                  'tenant_not_allowed',
                )
                .having((error) => error.message, 'message', 'tenant blocked')
                .having(
                  (error) => error.details?['allowed_tenants'],
                  'details',
                  ['gp'],
                ),
          ),
        );
      },
    );

    test(
      'acceptOidcLogin error는 code/message/details를 AuthProxyException으로 보존한다',
      () async {
        client.enqueueJson({
          'code': 'tenant_not_allowed',
          'error': 'tenant blocked',
          'details': {
            'allowed_tenants': ['gp'],
          },
        }, statusCode: 403);

        await expectLater(
          AuthProxyService.acceptOidcLogin('login-challenge', token: 'jwt'),
          throwsA(
            isA<AuthProxyException>()
                .having(
                  (error) => error.errorCode,
                  'code',
                  'tenant_not_allowed',
                )
                .having((error) => error.message, 'message', 'tenant blocked')
                .having(
                  (error) => error.details?['allowed_tenants'],
                  'details',
                  ['gp'],
                ),
          ),
        );
      },
    );

    test(
      'approveQrLogin은 credential mode와 bearer token payload를 지원한다',
      () async {
        client.enqueueJson({'ok': true});

        await AuthProxyService.approveQrLogin(
          'pending-qr',
          token: 'jwt-token',
          withCredentials: true,
        );

        expect(client.withCredentialsCalls, [true]);
        expect(client.lastJsonBody, {
          'pendingRef': 'pending-qr',
          'token': 'jwt-token',
        });
      },
    );

    test('createUser는 내부 도메인 personal 정책 오류를 한국어 안내로 표시한다', () async {
      client.enqueueJson({
        'error':
            'internal email domain cannot be assigned to personal tenant: user@hanmaceng.co.kr',
      }, statusCode: 400);

      await expectLater(
        AuthProxyService.createUser(
          loginId: 'user@hanmaceng.co.kr',
          adminPassword: 'admin-pass',
          email: 'user@hanmaceng.co.kr',
        ),
        throwsA(
          isA<Exception>().having(
            (error) => error.toString(),
            'message',
            contains('대표소속을 회사 또는 조직 소속으로 지정해 주세요'),
          ),
        ),
      );
    });

    test('updateUserDetails는 내부 도메인 personal 정책 오류를 한국어 안내로 표시한다', () async {
      client.enqueueJson({
        'error': '내부 도메인 사용자는 개인 소속으로 생성하거나 변경할 수 없습니다: user@brsw.kr',
      }, statusCode: 400);

      await expectLater(
        AuthProxyService.updateUserDetails(
          adminPassword: 'admin-pass',
          loginId: 'user@brsw.kr',
          email: 'user@brsw.kr',
        ),
        throwsA(
          isA<Exception>().having(
            (error) => error.toString(),
            'message',
            contains('대표소속을 회사 또는 조직 소속으로 지정해 주세요'),
          ),
        ),
      );
    });

    test('sendLog는 민감 정보를 제거한 client log를 전송한다', () async {
      client.enqueueJson({'ok': true});

      await AuthProxyService.sendLog(
        'warn',
        'token=secret password=hidden',
        data: {'authorization': 'Bearer secret', 'safe': 'value'},
      );

      expect(client.requests.single.url.path, '/api/v1/client-log');
      final body = client.lastJsonBody;
      expect(body['level'], 'warn');
      expect(body['message'], isNot(contains('secret')));
      expect((body['data'] as Map<String, dynamic>)['safe'], 'value');
      expect(
        (body['data'] as Map<String, dynamic>)['authorization'],
        isNot(contains('secret')),
      );
    });

    test('주요 성공 API는 method, path, payload 계약을 유지한다', () async {
      client
        ..enqueueJson({'minLength': 12})
        ..enqueueJson({'id': 'user-1'})
        ..enqueueJson({'slug': 'tenant'})
        ..enqueueJson({'verified': true})
        ..enqueueJson({'verified': true})
        ..enqueueJson({'redirect_to': '/callback'})
        ..enqueueJson({'redirect_to': '/consent'})
        ..enqueueJson({'redirect_to': '/rejected'})
        ..enqueueJson({'redirect_to': '/oidc'})
        ..enqueueJson({'pendingRef': 'reset-pending'})
        ..enqueueJson({'ok': true})
        ..enqueueJson({'ok': true})
        ..enqueueJson({'verified': true})
        ..enqueueJson({'pendingRef': 'qr'})
        ..enqueueJson({'status': 'pending'})
        ..enqueueJson({'ok': true})
        ..enqueueJson({'ok': true})
        ..enqueueJson({
          'users': [
            {'loginId': 'user@example.com'},
          ],
        })
        ..enqueueJson({'ok': true})
        ..enqueueJson({'ok': true})
        ..enqueueJson({'ok': true})
        ..enqueueJson({
          'items': [
            {'client_id': 'rp-1'},
          ],
        })
        ..enqueueJson({'ok': true})
        ..enqueueJson({'available': true})
        ..enqueueJson({'available': true, 'message': 'ok'})
        ..enqueueJson({'message': 'duplicated'}, statusCode: 409)
        ..enqueueAny([
          {'slug': 'gp'},
        ])
        ..enqueueJson({'ok': true})
        ..enqueueJson({'ok': true})
        ..enqueueJson({'verified': true})
        ..enqueueJson({'ok': true});

      AuthTokenStore.setToken('jwt-token');

      expect((await AuthProxyService.fetchPasswordPolicy())['minLength'], 12);
      expect(
        await AuthProxyService.getSessionStatus(token: 'session-token'),
        200,
      );
      expect((await AuthProxyService.getTenantInfo())['slug'], 'tenant');
      expect(
        (await AuthProxyService.verifyMagicLink(
          'magic-token',
          verifyOnly: true,
        ))['verified'],
        isTrue,
      );
      expect(
        (await AuthProxyService.verifyLoginShortCode(
          'SHORT123',
          verifyOnly: true,
        ))['verified'],
        isTrue,
      );
      expect(
        (await AuthProxyService.loginWithPassword(
          'user@example.com',
          'password',
          loginChallenge: 'challenge-1',
        ))['redirect_to'],
        '/callback',
      );
      expect(
        (await AuthProxyService.acceptConsent(
          'consent-1',
          grantScope: ['openid'],
        ))['redirect_to'],
        '/consent',
      );
      expect(
        (await AuthProxyService.rejectConsent('consent-1'))['redirect_to'],
        '/rejected',
      );
      expect(
        (await AuthProxyService.acceptOidcLogin(
          'login-challenge',
          token: 'jwt-token',
        ))['redirect_to'],
        '/oidc',
      );
      expect(
        (await AuthProxyService.initiatePasswordReset(
          'user@example.com',
          drySend: true,
        ))['pendingRef'],
        'reset-pending',
      );
      expect(
        (await AuthProxyService.completePasswordReset(
          loginId: 'user@example.com',
          token: 'reset-token',
          newPassword: 'new-password',
        ))['ok'],
        isTrue,
      );
      await AuthProxyService.sendSms('01012345678');
      expect(
        (await AuthProxyService.verifySmsCode(
          '01012345678',
          '123456',
        ))['verified'],
        isTrue,
      );
      expect((await AuthProxyService.initQrLogin())['pendingRef'], 'qr');
      expect((await AuthProxyService.pollQrStatus('qr'))['status'], 'pending');
      expect(await AuthProxyService.checkAdminAuth('admin-pass'), isTrue);
      await AuthProxyService.createUser(
        loginId: 'user@example.com',
        adminPassword: 'admin-pass',
        email: 'user@example.com',
        phone: '01012345678',
        displayName: 'User',
      );
      expect(await AuthProxyService.listUsers('admin-pass', query: 'user'), [
        {'loginId': 'user@example.com'},
      ]);
      await AuthProxyService.deleteUser('admin-pass', 'user@example.com');
      await AuthProxyService.updateUserStatus(
        'admin-pass',
        'user@example.com',
        'disabled',
      );
      await AuthProxyService.updateUserDetails(
        adminPassword: 'admin-pass',
        loginId: 'user@example.com',
        email: 'user2@example.com',
        phone: '01099998888',
        displayName: 'User 2',
      );
      expect(await AuthProxyService.fetchLinkedRps(), [
        {'client_id': 'rp-1'},
      ]);
      await AuthProxyService.revokeLinkedRp('rp-1');
      expect(
        await AuthProxyService.checkEmailAvailability('new@example.com'),
        isTrue,
      );
      expect(
        await AuthProxyService.checkLoginIDAvailability(
          'new@example.com',
          tenantSlug: 'gp',
        ),
        {'available': true, 'message': 'ok'},
      );
      expect(
        await AuthProxyService.checkLoginIDAvailability('dup@example.com'),
        {'available': false, 'message': 'duplicated'},
      );
      expect(
        await AuthProxyService.getActiveTenants(email: 'user@example.com'),
        [
          {'slug': 'gp'},
        ],
      );
      await AuthProxyService.sendSignupCode('user@example.com', 'email');
      await AuthProxyService.sendSignupCode('01012345678', 'sms');
      expect(
        (await AuthProxyService.verifySignupCode(
          'user@example.com',
          'email',
          '123456',
        ))['verified'],
        isTrue,
      );
      await AuthProxyService.signup(
        email: 'user@example.com',
        password: 'password',
        name: 'User',
        phone: '01012345678',
        affiliationType: 'tenant',
        tenantSlug: 'gp',
        department: 'R&D',
        termsAccepted: true,
      );

      expect(
        client.requests.map((request) => request.method),
        containsAll(['GET', 'POST', 'PATCH', 'DELETE']),
      );
      expect(
        client.requests.map((request) => request.url.path),
        containsAll([
          '/api/v1/auth/password/policy',
          '/api/v1/auth/magic-link/verify',
          '/api/v1/auth/login/code/verify-short',
          '/api/v1/auth/password/login',
          '/api/v1/auth/consent/accept',
          '/api/v1/auth/consent/reject',
          '/api/v1/auth/oidc/login/accept',
          '/api/v1/auth/password/reset/initiate',
          '/api/v1/auth/password/reset/complete',
          '/api/v1/auth/sms',
          '/api/v1/auth/verify-sms',
          '/api/v1/auth/qr/init',
          '/api/v1/auth/qr/poll',
          '/api/v1/admin/check',
          '/api/v1/admin/users',
          '/api/v1/user/rp/linked',
          '/api/v1/auth/signup/check-email',
          '/api/v1/auth/signup/check-login-id',
          '/api/v1/auth/signup/tenants',
          '/api/v1/auth/signup/send-email-code',
          '/api/v1/auth/signup/send-sms-code',
          '/api/v1/auth/signup/verify-code',
          '/api/v1/auth/signup',
        ]),
      );
    });

    test('대표 실패 API는 예외와 fallback 값을 반환한다', () async {
      client
        ..enqueueJson({'error': 'bad policy'}, statusCode: 500)
        ..enqueueJson({'error': 'profile failed'}, statusCode: 401)
        ..enqueueJson({'error': 'tenant failed'}, statusCode: 500)
        ..enqueueJson({'error': 'init failed'}, statusCode: 500)
        ..enqueueJson({'error': 'poll failed'}, statusCode: 500)
        ..enqueueJson({'error': 'magic failed'}, statusCode: 400)
        ..enqueueJson({'error': 'code failed'}, statusCode: 400)
        ..enqueueJson({'error': 'short failed'}, statusCode: 400)
        ..enqueueJson({'error': 'password failed'}, statusCode: 401)
        ..enqueueJson({'error': 'accept failed'}, statusCode: 400)
        ..enqueueJson({'error': 'reject failed'}, statusCode: 400)
        ..enqueueJson({'error': 'oidc failed'}, statusCode: 400)
        ..enqueueJson({'error': 'reset init failed'}, statusCode: 400)
        ..enqueueJson({'error': 'reset complete failed'}, statusCode: 400)
        ..enqueueJson({'error': 'sms failed'}, statusCode: 400)
        ..enqueueJson({'error': 'verify sms failed'}, statusCode: 400)
        ..enqueueJson({'error': 'qr init failed'}, statusCode: 400)
        ..enqueueJson({'error': 'qr poll failed'}, statusCode: 500)
        ..enqueueJson({'error': 'qr approve failed'}, statusCode: 400)
        ..enqueueJson({'error': 'create failed'}, statusCode: 400)
        ..enqueueJson({'error': 'list failed'}, statusCode: 400)
        ..enqueueJson({'error': 'delete failed'}, statusCode: 400)
        ..enqueueJson({'error': 'status failed'}, statusCode: 400)
        ..enqueueJson({'error': 'update failed'}, statusCode: 400)
        ..enqueueJson({'error': 'linked failed'}, statusCode: 500)
        ..enqueueJson({'error': 'revoke linked failed'}, statusCode: 400)
        ..enqueueJson({'available': false}, statusCode: 500)
        ..enqueueAny([], statusCode: 500)
        ..enqueueJson({'error': 'signup code failed'}, statusCode: 400)
        ..enqueueJson({'error': 'verify failed'}, statusCode: 400)
        ..enqueueJson({'error': 'signup failed'}, statusCode: 400);

      Future<void> expectThrows(Future<void> Function() action) async {
        await expectLater(action(), throwsA(isA<Object>()));
      }

      await expectThrows(() async => AuthProxyService.fetchPasswordPolicy());
      await expectThrows(() async => AuthProxyService.getMe(token: 'bad'));
      await expectThrows(() async => AuthProxyService.getTenantInfo());
      await expectThrows(
        () async => AuthProxyService.initEnchantedLink('user'),
      );
      await expectThrows(
        () async => AuthProxyService.pollEnchantedLink('pending'),
      );
      await expectThrows(() async => AuthProxyService.verifyMagicLink('magic'));
      await expectThrows(
        () async => AuthProxyService.verifyLoginCode('user', '123456'),
      );
      await expectThrows(
        () async => AuthProxyService.verifyLoginShortCode('SHORT'),
      );
      await expectThrows(
        () async => AuthProxyService.loginWithPassword('user', 'password'),
      );
      await expectThrows(() async => AuthProxyService.acceptConsent('consent'));
      await expectThrows(() async => AuthProxyService.rejectConsent('consent'));
      await expectThrows(() async => AuthProxyService.acceptOidcLogin('login'));
      await expectThrows(
        () async => AuthProxyService.initiatePasswordReset('user'),
      );
      await expectThrows(
        () async => AuthProxyService.completePasswordReset(newPassword: 'new'),
      );
      await expectThrows(() async => AuthProxyService.sendSms('01012345678'));
      await expectThrows(
        () async => AuthProxyService.verifySmsCode('01012345678', '123456'),
      );
      await expectThrows(() async => AuthProxyService.initQrLogin());
      await expectThrows(() async => AuthProxyService.pollQrStatus('pending'));
      await expectThrows(
        () async => AuthProxyService.approveQrLogin('pending'),
      );
      await expectThrows(
        () async => AuthProxyService.createUser(
          loginId: 'user',
          adminPassword: 'admin',
        ),
      );
      await expectThrows(() async => AuthProxyService.listUsers('admin'));
      await expectThrows(
        () async => AuthProxyService.deleteUser('admin', 'user'),
      );
      await expectThrows(
        () async =>
            AuthProxyService.updateUserStatus('admin', 'user', 'disabled'),
      );
      await expectThrows(
        () async => AuthProxyService.updateUserDetails(
          adminPassword: 'admin',
          loginId: 'user',
        ),
      );
      await expectThrows(() async => AuthProxyService.fetchLinkedRps());
      await expectThrows(() async => AuthProxyService.revokeLinkedRp('rp'));
      expect(
        await AuthProxyService.checkEmailAvailability('used@example.com'),
        isFalse,
      );
      expect(await AuthProxyService.getActiveTenants(), isEmpty);
      await expectThrows(
        () async =>
            AuthProxyService.sendSignupCode('user@example.com', 'email'),
      );
      await expectThrows(
        () async => AuthProxyService.verifySignupCode(
          'user@example.com',
          'email',
          '123456',
        ),
      );
      await expectThrows(
        () async => AuthProxyService.signup(
          email: 'user@example.com',
          password: 'password',
          name: 'User',
          phone: '01012345678',
          affiliationType: 'tenant',
          department: 'R&D',
          termsAccepted: true,
        ),
      );
    });
  });
}

class _QueuedResponse {
  const _QueuedResponse(this.statusCode, this.body);

  final int statusCode;
  final String body;
}

class _RecordingClient extends http.BaseClient {
  final requests = <http.BaseRequest>[];
  final withCredentialsCalls = <bool>[];
  final _responses = <_QueuedResponse>[];
  int closedCount = 0;

  void enqueueJson(Map<String, dynamic> body, {int statusCode = 200}) {
    enqueueAny(body, statusCode: statusCode);
  }

  void enqueueAny(Object? body, {int statusCode = 200}) {
    _responses.add(_QueuedResponse(statusCode, jsonEncode(body)));
  }

  Map<String, dynamic> get lastJsonBody {
    final request = requests.last as http.Request;
    return jsonDecode(request.body) as Map<String, dynamic>;
  }

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    requests.add(request);
    final response = _responses.isEmpty
        ? const _QueuedResponse(200, '{}')
        : _responses.removeAt(0);
    return http.StreamedResponse(
      Stream.value(utf8.encode(response.body)),
      response.statusCode,
      request: request,
      headers: {'content-type': 'application/json'},
    );
  }

  @override
  void close() {
    closedCount += 1;
    super.close();
  }
}
