import 'dart:convert';
import 'dart:io';

import 'package:block_seasons/data/remote/content_downloader.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';

void main() {
  late Directory tmp;

  setUp(() async {
    tmp = await Directory.systemTemp.createTemp('bs_cache_');
  });

  tearDown(() async {
    if (await tmp.exists()) await tmp.delete(recursive: true);
  });

  String shaOf(String body) => sha256.convert(utf8.encode(body)).toString();

  // Minimal pack body (repository parses later; downloader only verifies
  // bytes).
  const packBody = '{"schemaVersion":1,"seasonId":"season_002"}';

  Map<String, dynamic> manifestJson({String? sha, int version = 1}) => {
        'schemaVersion': 1,
        'minAppBuild': 1,
        'current': 'season_002',
        'seasons': [
          {
            'seasonId': 'season_002',
            'version': version,
            'packUrl': 'seasons/season_002/pack.json',
            'sha256': sha ?? shaOf(packBody),
          },
        ],
      };

  MockClient okClient({String? sha, int version = 1}) =>
      MockClient((request) async {
        if (request.url.path.endsWith('manifest.json')) {
          return http.Response(
            jsonEncode(manifestJson(sha: sha, version: version)),
            200,
            headers: {'content-type': 'application/json'},
          );
        }
        if (request.url.path.endsWith('pack.json')) {
          return http.Response(packBody, 200);
        }
        return http.Response('not found', 404);
      });

  test('downloads, verifies, and caches a new season', () async {
    final downloader = ContentDownloader(
      baseUrl: 'https://example.com/content',
      cacheDir: tmp,
      client: okClient(),
    );
    final updated = await downloader.sync();
    expect(updated, isTrue);
    final cached = File('${tmp.path}/seasons/season_002/pack.json');
    expect(await cached.readAsString(), packBody);
    expect(downloader.cachedVersion('season_002'), 1);
  });

  test('second sync with same version is a no-op', () async {
    final downloader = ContentDownloader(
      baseUrl: 'https://example.com/content',
      cacheDir: tmp,
      client: okClient(),
    );
    await downloader.sync();
    expect(await downloader.sync(), isFalse);
  });

  test('checksum mismatch rejects the pack and keeps cache clean', () async {
    final downloader = ContentDownloader(
      baseUrl: 'https://example.com/content',
      cacheDir: tmp,
      client: okClient(sha: 'deadbeef'),
    );
    final updated = await downloader.sync();
    expect(updated, isFalse);
    expect(
      File('${tmp.path}/seasons/season_002/pack.json').existsSync(),
      isFalse,
    );
    // No stray temp files left behind.
    final leftovers = tmp
        .listSync(recursive: true)
        .whereType<File>()
        .where((f) => f.path.endsWith('.tmp'));
    expect(leftovers, isEmpty);
  });

  test('network failure leaves existing cache untouched', () async {
    final good = ContentDownloader(
      baseUrl: 'https://example.com/content',
      cacheDir: tmp,
      client: okClient(),
    );
    await good.sync();

    final offline = ContentDownloader(
      baseUrl: 'https://example.com/content',
      cacheDir: tmp,
      client: MockClient((_) async => throw const SocketException('off')),
    );
    expect(await offline.sync(), isFalse);
    expect(
      File('${tmp.path}/seasons/season_002/pack.json').existsSync(),
      isTrue,
    );
  });

  test('version bump re-downloads', () async {
    await ContentDownloader(
      baseUrl: 'https://example.com/content',
      cacheDir: tmp,
      client: okClient(version: 1),
    ).sync();
    final updated = await ContentDownloader(
      baseUrl: 'https://example.com/content',
      cacheDir: tmp,
      client: okClient(version: 2),
    ).sync();
    expect(updated, isTrue);
  });

  test('malicious seasonId never escapes the cache dir', () async {
    final outside = Directory('${tmp.path}/../bs_escape_probe');
    addTearDown(() async {
      if (await outside.exists()) await outside.delete(recursive: true);
    });
    final client = MockClient((request) async {
      if (request.url.path.endsWith('manifest.json')) {
        return http.Response(
          jsonEncode({
            'schemaVersion': 1,
            'minAppBuild': 1,
            'current': 'x',
            'seasons': [
              {
                'seasonId': '../bs_escape_probe',
                'version': 1,
                'packUrl': 'seasons/x/pack.json',
                'sha256': shaOf(packBody),
              },
            ],
          }),
          200,
        );
      }
      return http.Response(packBody, 200);
    });
    final downloader = ContentDownloader(
      baseUrl: 'https://example.com/content',
      cacheDir: tmp,
      client: client,
    );
    expect(await downloader.sync(), isFalse);
    expect(await outside.exists(), isFalse);
  });

  test('corrupt cache index is treated as empty and re-downloads', () async {
    await File('${tmp.path}/cache_index.json').create(recursive: true);
    await File('${tmp.path}/cache_index.json').writeAsString('{broken');
    final downloader = ContentDownloader(
      baseUrl: 'https://example.com/content',
      cacheDir: tmp,
      client: okClient(),
    );
    expect(await downloader.sync(), isTrue);
    expect(downloader.cachedVersion('season_002'), 1);
  });
}
