import 'dart:io';

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plezy/models/shader_preset.dart';
import 'package:plezy/services/shader_asset_loader.dart';

import '../test_helpers/io_fakes.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late PathProviderPlatform originalPathProvider;
  late Directory root;
  late Directory supportDirectory;
  late File sentinel;

  setUp(() async {
    originalPathProvider = PathProviderPlatform.instance;
    root = await Directory.systemTemp.createTemp('plezy_shader_asset_loader_test_');
    PathProviderPlatform.instance = FakePathProvider(root);
    supportDirectory = Directory(path.join(root.path, 'support'))..createSync(recursive: true);
    sentinel = File(path.join(supportDirectory.path, 'sentinel.glsl'))..writeAsStringSync('sentinel');
    ShaderAssetLoader.clearCache();
  });

  tearDown(() async {
    PathProviderPlatform.instance = originalPathProvider;
    ShaderAssetLoader.clearCache();
    if (await root.exists()) await root.delete(recursive: true);
  });

  ShaderPreset customPreset(String fileName) {
    return ShaderPreset(id: 'custom_$fileName', name: fileName, type: ShaderPresetType.custom, fileName: fileName);
  }

  Future<List<int>> bundledBytes(String assetPath) async {
    final data = await rootBundle.load('assets/shaders/$assetPath');
    return data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
  }

  Future<void> expectBundledFile(String filePath, String assetPath) async {
    expect(await File(filePath).readAsBytes(), await bundledBytes(assetPath));
  }

  test('traversal and absolute names cannot load or delete outside managed directory', () async {
    for (final fileName in ['../sentinel.glsl', sentinel.path]) {
      expect(await ShaderAssetLoader.getShadersForPreset(customPreset(fileName)), isEmpty);
      await ShaderAssetLoader.deleteCustomShader(fileName);
      expect(await sentinel.readAsString(), 'sentinel');
    }
  });

  test('repairs stale built-in bytes in the application cache without touching temporary storage', () async {
    final cacheFile = File(path.join(root.path, 'cache', 'shaders', 'nvscaler', 'NVScaler.glsl'))
      ..createSync(recursive: true)
      ..writeAsStringSync('stale');
    final oldTemporaryFile = File(path.join(root.path, 'temp', 'shaders', 'nvscaler', 'NVScaler.glsl'))
      ..createSync(recursive: true)
      ..writeAsStringSync('old temporary sentinel');

    final shaders = await ShaderAssetLoader.getShadersForPreset(ShaderPreset.nvscalerDefault);

    expect(shaders, [cacheFile.path]);
    await expectBundledFile(shaders.single, 'nvscaler/NVScaler.glsl');
    expect(await oldTemporaryFile.readAsString(), 'old temporary sentinel');
  });

  test('recovers a truncated built-in final through a complete staged replacement', () async {
    final targetDirectory = Directory(path.join(root.path, 'cache', 'shaders', 'nvscaler'))
      ..createSync(recursive: true);
    final target = File(path.join(targetDirectory.path, 'NVScaler.glsl'))..writeAsBytesSync([1, 2, 3]);
    final unrelatedPending = File('${target.path}.pending.interrupted')..writeAsBytesSync([1]);

    final shaders = await ShaderAssetLoader.getNVScalerShaders();

    expect(shaders, [target.path]);
    await expectBundledFile(target.path, 'nvscaler/NVScaler.glsl');
    expect(unrelatedPending.existsSync(), isTrue);
    final ownedPending = await targetDirectory
        .list()
        .where((entry) => entry.path.contains('.pending.') && entry.path != unrelatedPending.path)
        .toList();
    expect(ownedPending, isEmpty);
  });

  test('does not return a partial path when final-file promotion fails', () async {
    final target = Directory(path.join(root.path, 'cache', 'shaders', 'nvscaler', 'NVScaler.glsl'))
      ..createSync(recursive: true);

    expect(await ShaderAssetLoader.getNVScalerShaders(), isEmpty);

    final siblings = await target.parent.list().toList();
    expect(siblings.where((entry) => entry.path.contains('.pending.')), isEmpty);
    expect(target.existsSync(), isTrue);
  });

  test('coalesces concurrent built-in loads onto one complete final path', () async {
    final results = await Future.wait([ShaderAssetLoader.getNVScalerShaders(), ShaderAssetLoader.getNVScalerShaders()]);

    expect(results[0], results[1]);
    expect(results[0], hasLength(1));
    await expectBundledFile(results[0].single, 'nvscaler/NVScaler.glsl');
    final directory = File(results[0].single).parent;
    expect((await directory.list().toList()).where((entry) => entry.path.contains('.pending.')), isEmpty);
  });

  test('materializes representative built-in chains in MPV order with bundled bytes', () async {
    final nvscaler = await ShaderAssetLoader.getNVScalerShaders();
    final artcnn = await ShaderAssetLoader.getArtCNNShaders(
      const ArtCNNConfig(model: ArtCNNModel.c4f16, variant: ArtCNNVariant.denoise),
    );
    final anime4k = await ShaderAssetLoader.getAnime4KShaders(
      const Anime4KConfig(quality: Anime4KQuality.fast, mode: Anime4KMode.modeB),
    );

    final expected = {
      nvscaler.single: 'nvscaler/NVScaler.glsl',
      artcnn.single: 'artcnn/ArtCNN_C4F16_DN.glsl',
      anime4k[0]: 'anime4k/Anime4K_Clamp_Highlights.glsl',
      anime4k[1]: 'anime4k/Anime4K_Restore_CNN_M.glsl',
      anime4k[2]: 'anime4k/Anime4K_Upscale_CNN_x2_M.glsl',
      anime4k[3]: 'anime4k/Anime4K_AutoDownscalePre_x2.glsl',
    };
    expect(anime4k.map(path.basename).toList(), [
      'Anime4K_Clamp_Highlights.glsl',
      'Anime4K_Restore_CNN_M.glsl',
      'Anime4K_Upscale_CNN_x2_M.glsl',
      'Anime4K_AutoDownscalePre_x2.glsl',
    ]);
    for (final entry in expected.entries) {
      expect(path.isWithin(path.join(root.path, 'cache'), entry.key), isTrue);
      await expectBundledFile(entry.key, entry.value);
    }
  });

  test('repeats the restore pass in place for doubled Anime4K modes', () async {
    final shaders = await ShaderAssetLoader.getAnime4KShaders(
      const Anime4KConfig(quality: Anime4KQuality.fast, mode: Anime4KMode.modeBB),
    );

    expect(shaders.map(path.basename).toList(), [
      'Anime4K_Clamp_Highlights.glsl',
      'Anime4K_Restore_CNN_M.glsl',
      'Anime4K_Restore_CNN_M.glsl',
      'Anime4K_Upscale_CNN_x2_M.glsl',
      'Anime4K_AutoDownscalePre_x2.glsl',
    ]);
    expect(shaders[1], shaders[2]);
  });

  test('nested and non-GLSL names are rejected without touching matching files', () async {
    final customDirectory = Directory(path.join(supportDirectory.path, 'custom_shaders'))..createSync(recursive: true);
    final nested = File(path.join(customDirectory.path, 'subdir', 'name.glsl'))
      ..createSync(recursive: true)
      ..writeAsStringSync('nested');
    final extraExtension = File(path.join(customDirectory.path, 'name.glsl.txt'))..writeAsStringSync('extra');

    for (final fileName in ['subdir/name.glsl', r'subdir\name.glsl', '.', '..', 'name.glsl.txt', 'name.txt']) {
      expect(ShaderAssetLoader.isValidCustomShaderFileName(fileName), isFalse);
      expect(await ShaderAssetLoader.getShadersForPreset(customPreset(fileName)), isEmpty);
      await ShaderAssetLoader.deleteCustomShader(fileName);
    }

    expect(await nested.readAsString(), 'nested');
    expect(await extraExtension.readAsString(), 'extra');
  });

  test('non-GLSL import fails before creating a managed file', () async {
    final source = File(path.join(root.path, 'shader.txt'))..writeAsStringSync('not glsl');

    await expectLater(ShaderAssetLoader.importCustomShader(source.path), throwsArgumentError);

    final customDirectory = Directory(path.join(supportDirectory.path, 'custom_shaders'));
    expect(customDirectory.existsSync(), isFalse);
  });

  test('imports a direct UUID GLSL child and deletes only that file', () async {
    final source = File(path.join(root.path, 'shader.GLSL'))..writeAsStringSync('shader');

    final storedName = await ShaderAssetLoader.importCustomShader(source.path);
    expect(storedName, matches(RegExp(r'^[0-9a-f-]+\.glsl$')));
    expect(ShaderAssetLoader.isValidCustomShaderFileName(storedName), isTrue);

    final shaders = await ShaderAssetLoader.getShadersForPreset(customPreset(storedName));
    expect(shaders, hasLength(1));
    expect(path.equals(path.dirname(shaders.single), path.join(supportDirectory.path, 'custom_shaders')), isTrue);
    expect(await File(shaders.single).readAsString(), 'shader');

    await ShaderAssetLoader.deleteCustomShader(storedName);
    expect(File(shaders.single).existsSync(), isFalse);
    expect(await source.readAsString(), 'shader');
  });

  test('legacy base-36 managed names remain loadable and deletable', () async {
    const storedName = 'ks9p7.glsl';
    final customDirectory = Directory(path.join(supportDirectory.path, 'custom_shaders'))..createSync(recursive: true);
    final managedFile = File(path.join(customDirectory.path, storedName))..writeAsStringSync('legacy');

    expect(ShaderAssetLoader.isValidCustomShaderFileName(storedName), isTrue);
    final shaders = await ShaderAssetLoader.getShadersForPreset(customPreset(storedName));
    expect(shaders, hasLength(1));
    expect(path.equals(shaders.single, managedFile.path), isTrue);

    await ShaderAssetLoader.deleteCustomShader(storedName);
    expect(managedFile.existsSync(), isFalse);
  });
}
