import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:plezy/connection/connection.dart';
import 'package:plezy/models/plex/plex_home_user.dart';
import 'package:plezy/profiles/profile.dart';
import 'package:plezy/profiles/profile_connection.dart';
import 'package:plezy/providers/user_profile_provider.dart';
import 'package:plezy/services/multi_server_manager.dart';
import 'package:plezy/services/plex_auth_service.dart';
import 'package:plezy/utils/media_server_http_client.dart';

import '../test_helpers/prefs.dart';
import '../test_helpers/profile_stack.dart';

void main() {
  setUp(resetSharedPreferencesForTest);

  group('UserProfileProvider (settings-only)', () {
    test('starts with null settings', () {
      final p = UserProfileProvider();
      expect(p.profileSettings, isNull);
      p.dispose();
    });

    test('refreshProfileSettings without a stored token is a no-op', () async {
      final p = UserProfileProvider();
      var notified = 0;
      p.addListener(() => notified++);
      await p.refreshProfileSettings();
      // No token → no API call → no notify, no error.
      expect(notified, 0);
      expect(p.profileSettings, isNull);
      p.dispose();
    });

    test('logout without initialization is safe', () async {
      final p = UserProfileProvider();
      await p.logout();
      expect(p.profileSettings, isNull);
      p.dispose();
    });

    test('safeNotifyListeners after dispose does not throw', () async {
      final p = UserProfileProvider();
      p.dispose();
      await p.logout();
    });

    test('settings connection follows the profile default row', () async {
      final stack = await ProfileStack.create();
      final manager = MultiServerManager();
      addTearDown(() async {
        manager.dispose();
        await stack.dispose();
      });

      final profile = Profile.local(id: 'local-owner', displayName: 'Owner', createdAt: DateTime(2026, 1, 1));
      final plex = PlexAccountConnection(
        id: 'plex-a',
        accountToken: 'plex-token',
        clientIdentifier: 'client-a',
        accountLabel: 'Plex',
        createdAt: DateTime(2026, 1, 1),
      );
      final jellyfin = JellyfinConnection(
        id: 'jf-machine/user-a',
        baseUrl: 'https://jf.example.com',
        serverName: 'Jellyfin',
        serverMachineId: 'jf-machine',
        userId: 'user-a',
        userName: 'User A',
        accessToken: 'jf-token',
        deviceId: 'device-a',
        createdAt: DateTime(2026, 1, 1),
      );
      await stack.profiles.upsert(profile);
      await stack.connections.upsert(plex);
      await stack.connections.upsert(jellyfin);
      await stack.profileConnections.upsert(
        ProfileConnection(
          profileId: profile.id,
          connectionId: plex.id,
          userToken: 'plex-user-token',
          userIdentifier: 'plex-user',
          isDefault: true,
        ),
        makeDefault: true,
      );
      await stack.profileConnections.upsert(
        ProfileConnection(profileId: profile.id, connectionId: jellyfin.id, userIdentifier: jellyfin.userId),
      );
      await stack.storage.setActiveProfileId(profile.id);
      await stack.active.initialize();

      final p = UserProfileProvider()
        ..attach(
          connections: stack.connections,
          activeProfile: stack.active,
          profileConnections: stack.profileConnections,
          serverManager: manager,
        );
      addTearDown(p.dispose);

      expect(await p.debugResolveActiveSettingsConnectionForTesting(), isA<PlexAccountConnection>());

      await stack.profileConnections.setDefault(profile.id, jellyfin.id);

      expect(await p.debugResolveActiveSettingsConnectionForTesting(), isA<JellyfinConnection>());
    });

    test('watches Plex Home profile connection rows', () async {
      final stack = await ProfileStack.create(
        homeUsers: [_homeUser(uuid: 'home-user-a', title: 'Home User')],
      );
      final manager = MultiServerManager();
      addTearDown(() async {
        manager.dispose();
        await stack.dispose();
      });

      final account = PlexAccountConnection(
        id: 'plex-a',
        accountToken: 'account-token-a',
        clientIdentifier: 'client-a',
        accountLabel: 'Plex A',
        createdAt: DateTime(2026, 1, 1),
      );
      await stack.connections.upsert(account);
      await stack.plexHome.refresh(account);
      await stack.storage.setActiveProfileId(
        plexHomeProfileId(accountConnectionId: account.id, homeUserUuid: 'home-user-a'),
      );
      await stack.active.initialize();

      final p = UserProfileProvider()
        ..attach(
          connections: stack.connections,
          activeProfile: stack.active,
          profileConnections: stack.profileConnections,
          serverManager: manager,
        );
      addTearDown(p.dispose);

      expect(p.debugWatchedProfileConnectionProfileId, stack.active.activeId);
    });

    test('Plex Home profile without a switched token makes no user request', () async {
      final fixture = await _HomeProfileFixture.create();
      addTearDown(fixture.dispose);

      expect(await fixture.provider.debugResolveActivePlexUserTokenForTesting(), isNull);

      await fixture.provider.refreshProfileSettings();

      expect(fixture.requests, isEmpty);
      expect(fixture.provider.profileSettings, isNull);
    });

    test('Plex Home profile with an empty switched token makes no user request', () async {
      final fixture = await _HomeProfileFixture.create(switchedToken: '');
      addTearDown(fixture.dispose);

      expect(await fixture.provider.debugResolveActivePlexUserTokenForTesting(), isNull);

      await fixture.provider.refreshProfileSettings();

      expect(fixture.requests, isEmpty);
      expect(fixture.provider.profileSettings, isNull);
    });

    test('Plex Home profile requests and publishes settings with its exact switched token', () async {
      final fixture = await _HomeProfileFixture.create(switchedToken: 'switched-home-user-marker');
      addTearDown(fixture.dispose);

      expect(await fixture.provider.debugResolveActivePlexUserTokenForTesting(), 'switched-home-user-marker');

      await fixture.provider.refreshProfileSettings();

      expect(fixture.requests, hasLength(1));
      final request = fixture.requests.single;
      expect(request.url, Uri.parse('https://clients.plex.tv/api/v2/user'));
      expect(request.headers['X-Plex-Token'], 'switched-home-user-marker');
      expect(request.headers['X-Plex-Token'], isNot('parent-account-marker'));
      expect(fixture.provider.profileSettings?.autoSelectAudio, isFalse);
      expect(fixture.provider.profileSettings?.defaultAudioLanguage, 'jpn');
      expect(fixture.provider.profileSettings?.defaultAudioLanguages, ['jpn', 'eng']);
    });

    test('Plex token fallback uses the selected local profile account', () async {
      final stack = await ProfileStack.create();
      final manager = MultiServerManager();
      addTearDown(() async {
        manager.dispose();
        await stack.dispose();
      });

      final profile = Profile.local(id: 'local-owner', displayName: 'Owner', createdAt: DateTime(2026, 1, 1));
      final accountA = PlexAccountConnection(
        id: 'plex-a',
        accountToken: 'wrong-owner-token',
        clientIdentifier: 'client-a',
        accountLabel: 'Plex A',
        createdAt: DateTime(2026, 1, 1),
      );
      final accountB = PlexAccountConnection(
        id: 'plex-b',
        accountToken: 'selected-owner-token',
        clientIdentifier: 'client-b',
        accountLabel: 'Plex B',
        createdAt: DateTime(2026, 1, 1),
      );
      await stack.profiles.upsert(profile);
      await stack.connections.upsert(accountA);
      await stack.connections.upsert(accountB);
      await stack.profileConnections.upsert(
        ProfileConnection(
          profileId: profile.id,
          connectionId: accountB.id,
          userIdentifier: 'home-user-b',
          isDefault: true,
        ),
        makeDefault: true,
      );
      await stack.storage.setActiveProfileId(profile.id);
      await stack.active.initialize();

      final requests = <http.Request>[];
      final auth = _recordingAuth(requests, audioLanguage: 'fra');
      addTearDown(auth.dispose);

      final p = UserProfileProvider(authService: auth)
        ..attach(
          connections: stack.connections,
          activeProfile: stack.active,
          profileConnections: stack.profileConnections,
          serverManager: manager,
        );
      addTearDown(p.dispose);

      expect(await p.debugResolveActivePlexUserTokenForTesting(), 'selected-owner-token');
      await p.refreshProfileSettings();
      expect(requests, hasLength(1));
      expect(requests.single.headers['X-Plex-Token'], 'selected-owner-token');
      expect(requests.single.headers['X-Plex-Token'], isNot('wrong-owner-token'));
      expect(p.profileSettings?.defaultAudioLanguage, 'fra');
    });
  });
}

PlexHomeUser _homeUser({required String uuid, required String title}) {
  return PlexHomeUser(
    id: 1,
    uuid: uuid,
    title: title,
    thumb: '',
    hasPassword: false,
    restricted: false,
    updatedAt: null,
    admin: false,
    guest: true,
    protected: false,
  );
}

PlexAuthService _recordingAuth(List<http.Request> requests, {required String audioLanguage}) {
  return PlexAuthService.forTesting(
    http: MediaServerHttpClient(
      client: MockClient((request) async {
        requests.add(request);
        return http.Response(
          jsonEncode({
            'profile': {
              'autoSelectAudio': false,
              'defaultAudioAccessibility': 0,
              'defaultAudioLanguage': audioLanguage,
              'defaultAudioLanguages': [audioLanguage, 'eng'],
              'defaultSubtitleLanguage': 'eng',
              'defaultSubtitleLanguages': ['eng'],
              'autoSelectSubtitle': 0,
              'defaultSubtitleAccessibility': 0,
              'defaultSubtitleForced': 1,
              'watchedIndicator': 1,
              'mediaReviewsVisibility': 0,
              'mediaReviewsLanguages': ['eng'],
            },
          }),
          200,
          headers: {'content-type': 'application/json'},
        );
      }),
    ),
  );
}

class _HomeProfileFixture {
  _HomeProfileFixture({required this.stack, required this.auth, required this.provider, required this.requests});

  final ProfileStack stack;
  final PlexAuthService auth;
  final UserProfileProvider provider;
  final List<http.Request> requests;

  static Future<_HomeProfileFixture> create({String? switchedToken}) async {
    final stack = await ProfileStack.create(
      homeUsers: [_homeUser(uuid: 'home-user-a', title: 'Home User')],
    );
    final account = PlexAccountConnection(
      id: 'plex-parent',
      accountToken: 'parent-account-marker',
      clientIdentifier: 'client-a',
      accountLabel: 'Plex Parent',
      createdAt: DateTime(2026, 1, 1),
    );
    await stack.connections.upsert(account);
    await stack.plexHome.refresh(account);

    final activeId = plexHomeProfileId(accountConnectionId: account.id, homeUserUuid: 'home-user-a');
    if (switchedToken != null) {
      await stack.profileConnections.upsert(
        ProfileConnection(
          profileId: activeId,
          connectionId: account.id,
          userToken: switchedToken,
          userIdentifier: 'home-user-a',
          isDefault: true,
        ),
        makeDefault: true,
      );
    }
    await stack.storage.setActiveProfileId(activeId);
    await stack.active.initialize();

    final requests = <http.Request>[];
    final auth = _recordingAuth(requests, audioLanguage: 'jpn');
    final provider = UserProfileProvider(authService: auth)
      ..attach(
        connections: stack.connections,
        activeProfile: stack.active,
        profileConnections: stack.profileConnections,
      );
    return _HomeProfileFixture(stack: stack, auth: auth, provider: provider, requests: requests);
  }

  Future<void> dispose() async {
    provider.dispose();
    auth.dispose();
    await stack.dispose();
  }
}
