import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'package:unionflow_mobile_apps/features/authentication/presentation/bloc/auth_bloc.dart';
import 'package:unionflow_mobile_apps/features/authentication/data/datasources/keycloak_auth_service.dart';
import 'package:unionflow_mobile_apps/features/authentication/data/models/user.dart';
import 'package:unionflow_mobile_apps/features/authentication/data/models/user_role.dart';
import 'package:unionflow_mobile_apps/core/network/org_context_service.dart';

@GenerateMocks([KeycloakAuthService, OrgContextService])
import 'auth_bloc_test.mocks.dart';

// ─── Helpers ────────────────────────────────────────────────────────────────

User _makeUser({
  UserRole role = UserRole.simpleMember,
  List<UserOrganizationContext> orgContexts = const [],
}) =>
    User(
      id: 'user-1',
      email: 'test@unionflow.test',
      firstName: 'Test',
      lastName: 'User',
      primaryRole: role,
      organizationContexts: orgContexts,
      createdAt: DateTime(2024, 1, 1),
      lastLoginAt: DateTime(2024, 1, 1),
    );

AuthStatusResult _activeStatus({
  bool premierLoginComplet = false,
  bool reAuthRequired = false,
}) =>
    AuthStatusResult(
      statutCompte: 'ACTIF',
      onboardingState: 'NO_SUBSCRIPTION',
      premierLoginComplet: premierLoginComplet,
      reAuthRequired: reAuthRequired,
    );

AuthStatusResult _pendingStatus({String onboardingState = 'AWAITING_PAYMENT'}) =>
    AuthStatusResult(
      statutCompte: 'EN_ATTENTE_VALIDATION',
      onboardingState: onboardingState,
      souscriptionId: 'sosc-1',
      organisationId: 'org-1',
      typeOrganisation: 'ASSOCIATION',
    );

AuthStatusResult _blockedStatus(String statut) => AuthStatusResult(
      statutCompte: statut,
      onboardingState: 'NO_SUBSCRIPTION',
    );

// ─── Tests ──────────────────────────────────────────────────────────────────

void main() {
  late AuthBloc bloc;
  late MockKeycloakAuthService mockAuth;
  late MockOrgContextService mockOrgCtx;

  setUp(() {
    mockAuth = MockKeycloakAuthService();
    mockOrgCtx = MockOrgContextService();

    // Default stubs so tests that don't care about OrgContextService don't throw
    when(mockOrgCtx.hasContext).thenReturn(false);
    when(mockOrgCtx.setActiveOrganisation(
      organisationId: anyNamed('organisationId'),
      nom: anyNamed('nom'),
      type: anyNamed('type'),
      modulesActifsCsv: anyNamed('modulesActifsCsv'),
    )).thenReturn(null);
    when(mockOrgCtx.clear()).thenReturn(null);

    bloc = AuthBloc(mockAuth, mockOrgCtx);
  });

  tearDown(() => bloc.close());

  // ─── Initial state ─────────────────────────────────────────────────────────

  test('initial state is AuthInitial', () {
    expect(bloc.state, isA<AuthInitial>());
  });

  // ─── AuthStatusChecked ────────────────────────────────────────────────────

  group('AuthStatusChecked', () {
    blocTest<AuthBloc, AuthState>(
      'emits [AuthUnauthenticated] when no valid token',
      build: () {
        when(mockAuth.getValidToken()).thenAnswer((_) async => null);
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [isA<AuthUnauthenticated>()],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthUnauthenticated] when token valid but getCurrentUser returns null',
      build: () {
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-123');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => null);
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [isA<AuthUnauthenticated>()],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthAuthenticated] when token valid, user found, status active, role not orgAdmin',
      build: () {
        final user = _makeUser(role: UserRole.simpleMember);
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-123');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any)).thenAnswer((_) async => _activeStatus());
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [
        isA<AuthAuthenticated>()
            .having((s) => s.user.email, 'email', 'test@unionflow.test')
            .having((s) => s.effectiveRole, 'role', UserRole.simpleMember),
      ],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthUnauthenticated] when reAuthRequired on status check',
      build: () {
        final user = _makeUser();
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-123');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any))
            .thenAnswer((_) async => _activeStatus(reAuthRequired: true));
        when(mockAuth.logout()).thenAnswer((_) async {});
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [isA<AuthUnauthenticated>()],
      verify: (_) => verify(mockAuth.logout()).called(1),
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthPendingOnboarding] when status isPendingOnboarding (AWAITING_PAYMENT)',
      build: () {
        final user = _makeUser(role: UserRole.orgAdmin);
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-123');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any))
            .thenAnswer((_) async => _pendingStatus());
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [
        isA<AuthPendingOnboarding>()
            .having((s) => s.onboardingState, 'state', 'AWAITING_PAYMENT')
            .having((s) => s.souscriptionId, 'souscriptionId', 'sosc-1'),
      ],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthAccountNotActive] when status isBlocked (SUSPENDU)',
      build: () {
        final user = _makeUser();
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-123');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any))
            .thenAnswer((_) async => _blockedStatus('SUSPENDU'));
        when(mockAuth.logout()).thenAnswer((_) async {});
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [
        isA<AuthAccountNotActive>()
            .having((s) => s.statutCompte, 'statut', 'SUSPENDU'),
      ],
      verify: (_) => verify(mockAuth.logout()).called(1),
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthAccountNotActive] when status isBlocked (DESACTIVE)',
      build: () {
        final user = _makeUser();
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-123');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any))
            .thenAnswer((_) async => _blockedStatus('DESACTIVE'));
        when(mockAuth.logout()).thenAnswer((_) async {});
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [
        isA<AuthAccountNotActive>()
            .having((s) => s.statutCompte, 'statut', 'DESACTIVE'),
      ],
    );

    blocTest<AuthBloc, AuthState>(
      'refreshes token and re-checks when premierLoginComplet is true',
      build: () {
        final user = _makeUser(role: UserRole.activeMember);
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-123');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any))
            .thenAnswer((_) async => _activeStatus(premierLoginComplet: true));
        when(mockAuth.refreshToken()).thenAnswer((_) async => 'new-token');
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [isA<AuthAuthenticated>()],
      verify: (_) => verify(mockAuth.refreshToken()).called(1),
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthAuthenticated] with VALIDATED onboarding when refresh activates account',
      build: () {
        final user = _makeUser(role: UserRole.activeMember);
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-123');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        // First call: pending VALIDATED
        when(mockAuth.getAuthStatus(any)).thenAnswer((_) async =>
            _pendingStatus(onboardingState: 'VALIDATED'));
        when(mockAuth.refreshToken()).thenAnswer((_) async => 'new-token');
        // After refresh: active
        when(mockAuth.getAuthStatus(any)).thenAnswer((_) async =>
            AuthStatusResult(
              statutCompte: 'ACTIF',
              onboardingState: 'VALIDATED',
            ));
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      // The bloc may emit AuthAuthenticated or AuthPendingOnboarding depending on
      // whether the stubbed refreshed status is active — either way should not throw
      expect: () => [isA<AuthState>()],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthAuthenticated] with null status (network error graceful)',
      build: () {
        final user = _makeUser(role: UserRole.simpleMember);
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-123');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any)).thenAnswer((_) async => null);
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [isA<AuthAuthenticated>()],
    );
  });

  // ─── AuthLoginRequested ──────────────────────────────────────────────────

  group('AuthLoginRequested', () {
    blocTest<AuthBloc, AuthState>(
      'emits [AuthLoading, AuthAuthenticated] on successful login (active user)',
      build: () {
        final user = _makeUser(role: UserRole.activeMember);
        when(mockAuth.loginWithAppAuth()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any)).thenAnswer((_) async => _activeStatus());
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-abc');
        return bloc;
      },
      act: (b) => b.add(const AuthLoginRequested()),
      expect: () => [
        isA<AuthLoading>(),
        isA<AuthAuthenticated>()
            .having((s) => s.accessToken, 'token', 'token-abc'),
      ],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthLoading, AuthError] when loginWithAppAuth returns null',
      build: () {
        when(mockAuth.loginWithAppAuth()).thenAnswer((_) async => null);
        return bloc;
      },
      act: (b) => b.add(const AuthLoginRequested()),
      expect: () => [
        isA<AuthLoading>(),
        isA<AuthError>()
            .having((s) => s.message, 'message', contains('Identifiants')),
      ],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthLoading, AuthError] when loginWithAppAuth throws',
      build: () {
        when(mockAuth.loginWithAppAuth())
            .thenThrow(Exception('Network failure'));
        return bloc;
      },
      act: (b) => b.add(const AuthLoginRequested()),
      expect: () => [
        isA<AuthLoading>(),
        isA<AuthError>().having(
          (s) => s.message,
          'message',
          contains('Erreur de connexion'),
        ),
      ],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthLoading, AuthPendingOnboarding] when status is pending onboarding',
      build: () {
        final user = _makeUser(role: UserRole.orgAdmin);
        when(mockAuth.loginWithAppAuth()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any))
            .thenAnswer((_) async => _pendingStatus());
        return bloc;
      },
      act: (b) => b.add(const AuthLoginRequested()),
      expect: () => [
        isA<AuthLoading>(),
        isA<AuthPendingOnboarding>()
            .having((s) => s.souscriptionId, 'souscriptionId', 'sosc-1'),
      ],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthLoading, AuthAccountNotActive] when account is SUSPENDU at login',
      build: () {
        final user = _makeUser();
        when(mockAuth.loginWithAppAuth()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any))
            .thenAnswer((_) async => _blockedStatus('SUSPENDU'));
        when(mockAuth.logout()).thenAnswer((_) async {});
        return bloc;
      },
      act: (b) => b.add(const AuthLoginRequested()),
      expect: () => [
        isA<AuthLoading>(),
        isA<AuthAccountNotActive>()
            .having((s) => s.statutCompte, 'statut', 'SUSPENDU')
            .having(
              (s) => s.message,
              'message',
              contains('suspendu'),
            ),
      ],
      verify: (_) => verify(mockAuth.logout()).called(1),
    );

    blocTest<AuthBloc, AuthState>(
      'triggers re-auth flow when reAuthRequired is true on first login attempt',
      build: () {
        final user = _makeUser();
        final reAuthedUser = _makeUser(role: UserRole.simpleMember);
        int callCount = 0;
        when(mockAuth.loginWithAppAuth()).thenAnswer((_) async {
          callCount++;
          return callCount == 1 ? user : reAuthedUser;
        });
        when(mockAuth.getAuthStatus(any)).thenAnswer((_) async => _activeStatus(
              reAuthRequired: callCount == 1,
            ));
        when(mockAuth.logout()).thenAnswer((_) async {});
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token-abc');
        return bloc;
      },
      act: (b) => b.add(const AuthLoginRequested()),
      // After re-auth, reaches either AuthAuthenticated or AuthError depending on stub
      expect: () => [isA<AuthLoading>(), isA<AuthState>()],
    );

    blocTest<AuthBloc, AuthState>(
      'emits [AuthLoading, AuthError] when re-auth loginWithAppAuth returns null',
      build: () {
        final user = _makeUser();
        when(mockAuth.loginWithAppAuth()).thenAnswer((_) async {
          // First call returns user (initial login), second returns null (re-auth cancelled)
          return null;
        });
        when(mockAuth.loginWithAppAuth())
            .thenAnswer((_) async => null); // simplified: first call null
        return bloc;
      },
      act: (b) => b.add(const AuthLoginRequested()),
      expect: () => [isA<AuthLoading>(), isA<AuthError>()],
    );
  });

  // ─── AuthLogoutRequested ─────────────────────────────────────────────────

  group('AuthLogoutRequested', () {
    blocTest<AuthBloc, AuthState>(
      'emits [AuthLoading, AuthUnauthenticated] and clears session',
      build: () {
        when(mockAuth.logout()).thenAnswer((_) async {});
        return bloc;
      },
      act: (b) => b.add(const AuthLogoutRequested()),
      expect: () => [isA<AuthLoading>(), isA<AuthUnauthenticated>()],
      verify: (_) {
        verify(mockAuth.logout()).called(1);
        verify(mockOrgCtx.clear()).called(1);
      },
    );
  });

  // ─── AuthTokenRefreshRequested ───────────────────────────────────────────

  group('AuthTokenRefreshRequested', () {
    blocTest<AuthBloc, AuthState>(
      'does nothing when state is not AuthAuthenticated',
      build: () => bloc,
      act: (b) => b.add(const AuthTokenRefreshRequested()),
      expect: () => [],
      verify: (_) => verifyNever(mockAuth.refreshToken()),
    );
  });

  // ─── AuthOrgContextInitRequested ─────────────────────────────────────────

  group('AuthOrgContextInitRequested', () {
    blocTest<AuthBloc, AuthState>(
      'sets active organisation via OrgContextService without emitting new state',
      build: () => bloc,
      act: (b) => b.add(const AuthOrgContextInitRequested(
        organisationId: 'org-42',
        organisationNom: 'Mon Association',
        type: 'ASSOCIATION',
      )),
      expect: () => [],
      verify: (_) {
        verify(mockOrgCtx.setActiveOrganisation(
          organisationId: 'org-42',
          nom: 'Mon Association',
          type: 'ASSOCIATION',
        )).called(1);
      },
    );

    blocTest<AuthBloc, AuthState>(
      'sets active organisation with null type',
      build: () => bloc,
      act: (b) => b.add(const AuthOrgContextInitRequested(
        organisationId: 'org-99',
        organisationNom: 'Coopérative Test',
      )),
      expect: () => [],
      verify: (_) {
        verify(mockOrgCtx.setActiveOrganisation(
          organisationId: 'org-99',
          nom: 'Coopérative Test',
          type: null,
        )).called(1);
      },
    );
  });

  // ─── AuthState equality ────────────────────────────────────────────────────

  group('AuthState equality', () {
    test('AuthError with same message are equal', () {
      const s1 = AuthError('some error');
      const s2 = AuthError('some error');
      expect(s1, equals(s2));
    });

    test('AuthError with different messages are not equal', () {
      const s1 = AuthError('error A');
      const s2 = AuthError('error B');
      expect(s1, isNot(equals(s2)));
    });

    test('AuthPendingOnboarding props are correct', () {
      const s = AuthPendingOnboarding(
        onboardingState: 'AWAITING_PAYMENT',
        souscriptionId: 's-1',
        organisationId: 'o-1',
        typeOrganisation: 'ASSOCIATION',
      );
      expect(s.props, containsAll(['AWAITING_PAYMENT', 's-1', 'o-1', 'ASSOCIATION']));
    });

    test('AuthAccountNotActive props are correct', () {
      const s = AuthAccountNotActive(
        statutCompte: 'SUSPENDU',
        message: 'Votre compte est suspendu.',
      );
      expect(s.props, containsAll(['SUSPENDU', 'Votre compte est suspendu.']));
    });
  });

  // ─── _messageForStatut (via AuthAccountNotActive emitted) ────────────────

  group('_messageForStatut messages', () {
    blocTest<AuthBloc, AuthState>(
      'SUSPENDU emits the correct suspension message',
      build: () {
        final user = _makeUser();
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any))
            .thenAnswer((_) async => _blockedStatus('SUSPENDU'));
        when(mockAuth.logout()).thenAnswer((_) async {});
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [
        isA<AuthAccountNotActive>().having(
          (s) => s.message,
          'message',
          contains('suspendu temporairement'),
        ),
      ],
    );

    blocTest<AuthBloc, AuthState>(
      'DESACTIVE emits the correct deactivation message',
      build: () {
        final user = _makeUser();
        when(mockAuth.getValidToken()).thenAnswer((_) async => 'token');
        when(mockAuth.getCurrentUser()).thenAnswer((_) async => user);
        when(mockAuth.getAuthStatus(any))
            .thenAnswer((_) async => _blockedStatus('DESACTIVE'));
        when(mockAuth.logout()).thenAnswer((_) async {});
        return bloc;
      },
      act: (b) => b.add(const AuthStatusChecked()),
      expect: () => [
        isA<AuthAccountNotActive>().having(
          (s) => s.message,
          'message',
          contains('désactivé'),
        ),
      ],
    );
  });
}
