using System.IO.Compression;
using Kompas.Mcp.Core.Interop;

namespace Kompas.Mcp.Tests;

/// <summary>
/// Поиск интероп-сборок КОМПАС: приоритет источников, распаковка архива SDK, тексты ошибок.
/// Настоящие DLL не нужны — провайдер проверяет только наличие файлов с нужными именами.
/// </summary>
[Trait("Category", "Unit")]
public sealed class KompasInteropProviderTests : IDisposable
{
    private static readonly string[] RequiredFiles =
    {
        "KompasAPI7.dll",
        "Kompas6API5.dll",
        "Kompas6Constants.dll",
        "Kompas6Constants3D.dll",
    };

    private readonly string _root = Path.Combine(Path.GetTempPath(), "kompas-interop-tests-" + Guid.NewGuid().ToString("N")[..8]);

    public void Dispose()
    {
        try { if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true); } catch { }
    }

    [Theory]
    [InlineData("KompasAPI7", true)]
    [InlineData("kompas6api5", true)]
    [InlineData("Kompas6API3D5COM", true)]
    [InlineData("Kompas.Mcp.Core", false)]
    [InlineData(null, false)]
    public void IsKompasAssembly_matches_only_interop_names(string? name, bool expected)
        => Assert.Equal(expected, KompasInteropProvider.IsKompasAssembly(name));

    [Fact]
    public void Override_directory_wins_over_everything()
    {
        var overrideDir = CreateDirectoryWithAssemblies("override");
        var baseDir = CreateDirectoryWithAssemblies("base");

        var provider = new KompasInteropProvider(overrideDir, baseDir, Cache, () => throw new Xunit.Sdk.XunitException(
            "поиск установки не должен вызываться при заданном KOMPAS_INTEROP_DIR"));

        Assert.Equal(overrideDir, provider.ResolveDirectory());
    }

    [Fact]
    public void Override_directory_without_assemblies_fails_loudly()
    {
        var empty = Path.Combine(_root, "empty");
        Directory.CreateDirectory(empty);

        var provider = new KompasInteropProvider(empty, MissingDirectory, Cache, () => null);

        var error = Assert.Throws<KompasInteropException>(() => provider.ResolveDirectory());
        Assert.Contains(KompasInteropProvider.OverrideVariable, error.Message, StringComparison.Ordinal);
        Assert.Contains(empty, error.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Base_directory_is_used_when_it_holds_assemblies()
    {
        var baseDir = CreateDirectoryWithAssemblies("base");

        var provider = new KompasInteropProvider(null, baseDir, Cache, () => throw new Xunit.Sdk.XunitException(
            "поиск установки не должен вызываться, когда сборки лежат рядом с сервером"));

        Assert.Equal(baseDir, provider.ResolveDirectory());
    }

    [Fact]
    public void Missing_installation_reports_actionable_reason()
    {
        var provider = new KompasInteropProvider(null, MissingDirectory, Cache, () => null);

        var error = Assert.Throws<KompasInteropException>(() => provider.ResolveDirectory());
        Assert.Contains("KOMPAS.Application", error.Message, StringComparison.Ordinal);
        Assert.Contains(KompasInteropProvider.OverrideVariable, error.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Installation_without_sdk_component_reports_missing_archive()
    {
        var install = Path.Combine(_root, "KOMPAS-3D v24 Home");
        Directory.CreateDirectory(install);

        var provider = new KompasInteropProvider(null, MissingDirectory, Cache, () => install);

        var error = Assert.Throws<KompasInteropException>(() => provider.ResolveDirectory());
        Assert.Contains("SDK", error.Message, StringComparison.Ordinal);
        Assert.Contains("CSharp.zip", error.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Archive_without_expected_entries_reports_empty_common_folder()
    {
        var install = CreateInstallation("KOMPAS-3D v24 Home", entries: new[] { "Automation/Interop.KGAXLib.dll" });

        var error = Assert.Throws<KompasInteropException>(
            () => new KompasInteropProvider(null, MissingDirectory, Cache, () => install).ResolveDirectory());
        Assert.Contains("Common/", error.Message, StringComparison.Ordinal);

        // Повтор в новом процессе не должен спотыкаться об оставшийся негодный каталог кеша.
        var repeated = Assert.Throws<KompasInteropException>(
            () => new KompasInteropProvider(null, MissingDirectory, Cache, () => install).ResolveDirectory());
        Assert.Equal(error.Message, repeated.Message);
    }

    [Fact]
    public void Assemblies_are_extracted_from_sdk_archive_into_cache()
    {
        var install = CreateInstallation("KOMPAS-3D v24 Home", entries: CommonEntries("readme.txt"));

        var directory = new KompasInteropProvider(null, MissingDirectory, Cache, () => install).ResolveDirectory();

        Assert.StartsWith(Cache, directory, StringComparison.Ordinal);
        Assert.All(RequiredFiles, file => Assert.True(File.Exists(Path.Combine(directory, file)), file));
        // В кеш попадают только DLL из Common — не весь архив.
        Assert.False(File.Exists(Path.Combine(directory, "readme.txt")));
        Assert.False(File.Exists(Path.Combine(directory, "Interop.KGAXLib.dll")));
    }

    [Fact]
    public void Extracted_cache_is_reused_by_the_next_process()
    {
        var install = CreateInstallation("KOMPAS-3D v24 Home", entries: CommonEntries());

        var first = new KompasInteropProvider(null, MissingDirectory, Cache, () => install).ResolveDirectory();
        var marker = Path.Combine(first, "marker.txt");
        File.WriteAllText(marker, "распаковано первым запуском");

        var second = new KompasInteropProvider(null, MissingDirectory, Cache, () => install).ResolveDirectory();

        Assert.Equal(first, second);
        Assert.True(File.Exists(marker), "кеш должен переиспользоваться, а не распаковываться заново");
    }

    [Fact]
    public void Cache_key_follows_the_sdk_archive()
    {
        var archive = Path.Combine(_root, "CSharp.zip");
        Directory.CreateDirectory(_root);
        File.WriteAllText(archive, "v24");
        var before = KompasInteropProvider.CacheKey(new FileInfo(archive));

        // Обновление КОМПАС меняет архив — ключ обязан смениться, иначе подхватится старый кеш.
        File.WriteAllText(archive, "v25 — другой размер");
        File.SetLastWriteTimeUtc(archive, DateTime.UnixEpoch.AddDays(1));
        var after = KompasInteropProvider.CacheKey(new FileInfo(archive));

        Assert.NotEqual(before, after);
        Assert.Equal(16, before.Length);
    }

    private string Cache => Path.Combine(_root, "cache");

    private string MissingDirectory => Path.Combine(_root, "no-such-directory");

    private static string[] CommonEntries(params string[] extra)
        => RequiredFiles.Select(file => "Common/" + file)
                        .Concat(extra.Select(name => "Common/" + name))
                        .Append("Automation/Interop.KGAXLib.dll")
                        .ToArray();

    private string CreateDirectoryWithAssemblies(string name)
    {
        var directory = Path.Combine(_root, name);
        Directory.CreateDirectory(directory);
        foreach (var file in RequiredFiles)
            File.WriteAllText(Path.Combine(directory, file), "заглушка");

        return directory;
    }

    private string CreateInstallation(string name, string[] entries)
    {
        var install = Path.Combine(_root, name);
        var samples = Path.Combine(install, "SDK", "Samples");
        Directory.CreateDirectory(samples);

        using var archive = ZipFile.Open(Path.Combine(samples, "CSharp.zip"), ZipArchiveMode.Create);
        foreach (var entry in entries)
        {
            using var writer = new StreamWriter(archive.CreateEntry(entry).Open());
            writer.Write("заглушка");
        }

        return install;
    }
}
