// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Remote.Testing;
using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Remote.UnitTests;

[CollectionDefinition(Name)]
public sealed class AssemblyLoadTestFixtureCollection : ICollectionFixture<AssemblyLoadTestFixture>
{
    public const string Name = nameof(AssemblyLoadTestFixtureCollection);
    private AssemblyLoadTestFixtureCollection() { }
}

[Collection(AssemblyLoadTestFixtureCollection.Name)]
[UseExportProvider]
public sealed class SnapshotSerializationTests(AssemblyLoadTestFixture testFixture)
{
    private readonly AssemblyLoadTestFixture _testFixture = testFixture;

    private static Workspace CreateWorkspace(Type[] additionalParts = null)
        => new AdhocWorkspace(FeaturesTestCompositions.Features.AddParts(additionalParts).WithTestHostParts(TestHost.OutOfProcess).GetHostServices());

    internal static Solution SetFullSolution(Workspace workspace)
    {
        workspace.SetCurrentSolution(solution =>
        {
            var csCode = "class A { }";
            var project1 = solution.AddProject("Project", "Project.dll", LanguageNames.CSharp);
            var document1 = project1.AddDocument("Document1", SourceText.From(csCode));

            var vbCode = "Class B\r\nEnd Class";
            var project2 = document1.Project.Solution.AddProject("Project2", "Project2.dll", LanguageNames.VisualBasic);
            var document2 = project2.AddDocument("Document2", SourceText.From(vbCode));

            solution = document2.Project.Solution.GetRequiredProject(project1.Id)
                .AddProjectReference(new ProjectReference(project2.Id, ["test"]))
                .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
                .AddAnalyzerReference(new AnalyzerFileReference(Path.Combine(TempRoot.Root, "path1"), new TestAnalyzerAssemblyLoader()))
                .AddAdditionalDocument("Additional", SourceText.From("hello"), ["test"], @".\Add").Project.Solution;

            return solution
                .WithAnalyzerReferences([new AnalyzerFileReference(Path.Combine(TempRoot.Root, "path2"), new TestAnalyzerAssemblyLoader())])
                .AddAnalyzerConfigDocuments([DocumentInfo.Create(
                    DocumentId.CreateNewId(project1.Id),
                    ".editorconfig",
                    loader: TextLoader.From(TextAndVersion.Create(SourceText.From("root = true"), VersionStamp.Create())))]);
        }, WorkspaceChangeKind.SolutionChanged);

        return workspace.CurrentSolution;
    }

    private static async Task<SolutionAsset> GetRequiredAssetAsync(SolutionAssetStorage.Scope scope, Checksum checksum)
    {
        var data = await scope.GetTestAccessor().GetAssetAsync(checksum, CancellationToken.None);
        Contract.ThrowIfNull(data);
        return new(checksum, data);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Empty()
    {
        using var workspace = CreateWorkspace();
        var solution = workspace.CurrentSolution;

        var validator = new SerializationValidator(workspace.Services);

        using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
        var checksum = scope.SolutionChecksum;
        var solutionSyncObject = await GetRequiredAssetAsync(scope, checksum);

        await validator.VerifySynchronizationObjectInServiceAsync(solutionSyncObject);

        var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(checksum);
        var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);
        await validator.VerifyChecksumInServiceAsync(solutionObject.Attributes, WellKnownSynchronizationKind.SolutionAttributes);

        Assert.Equal(0, solutionObject.Projects.Length);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Empty_Serialization()
    {
        using var workspace = CreateWorkspace();
        var solution = workspace.CurrentSolution;

        var validator = new SerializationValidator(workspace.Services);
        using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
        await validator.VerifySolutionStateSerializationAsync(solution, scope.SolutionChecksum);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Project()
    {
        using var workspace = CreateWorkspace();
        var solution = workspace.CurrentSolution;
        var project = solution.AddProject("Project", "Project.dll", LanguageNames.CSharp);

        var validator = new SerializationValidator(workspace.Services);

        using var scope = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);
        var checksum = scope.SolutionChecksum;
        var solutionSyncObject = await GetRequiredAssetAsync(scope, checksum);

        await validator.VerifySynchronizationObjectInServiceAsync(solutionSyncObject);

        var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(checksum);
        var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);

        await validator.VerifyChecksumInServiceAsync(solutionObject.Attributes, WellKnownSynchronizationKind.SolutionAttributes);

        Assert.Equal(1, solutionObject.Projects.Length);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Project_Serialization()
    {
        using var workspace = CreateWorkspace();
        var project = workspace.CurrentSolution.AddProject("Project", "Project.dll", LanguageNames.CSharp);

        var validator = new SerializationValidator(workspace.Services);

        using var snapshot = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);
        await validator.VerifySolutionStateSerializationAsync(project.Solution, snapshot.SolutionChecksum);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId()
    {
        var code = "class A { }";

        using var workspace = CreateWorkspace();
        var document = workspace.CurrentSolution.AddProject("Project", "Project.dll", LanguageNames.CSharp).AddDocument("Document", SourceText.From(code));

        var validator = new SerializationValidator(workspace.Services);

        using var scope = await validator.AssetStorage.StoreAssetsAsync(document.Project.Solution, CancellationToken.None);
        var syncObject = await GetRequiredAssetAsync(scope, scope.SolutionChecksum);
        var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(syncObject.Checksum);
        var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);

        await validator.VerifySynchronizationObjectInServiceAsync(syncObject);
        await validator.VerifyChecksumInServiceAsync(solutionObject.Attributes, WellKnownSynchronizationKind.SolutionAttributes);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Serialization()
    {
        var code = "class A { }";

        using var workspace = CreateWorkspace();
        var solution = workspace.CurrentSolution;
        var document = solution.AddProject("Project", "Project.dll", LanguageNames.CSharp).AddDocument("Document", SourceText.From(code));

        var validator = new SerializationValidator(workspace.Services);

        using var scope = await validator.AssetStorage.StoreAssetsAsync(document.Project.Solution, CancellationToken.None);
        await validator.VerifySolutionStateSerializationAsync(document.Project.Solution, scope.SolutionChecksum);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Full()
    {
        using var workspace = CreateWorkspace();
        var solution = SetFullSolution(workspace);

        var firstProjectChecksum = await solution.GetProject(solution.ProjectIds[0]).State.GetChecksumAsync(CancellationToken.None);
        var secondProjectChecksum = await solution.GetProject(solution.ProjectIds[1]).State.GetChecksumAsync(CancellationToken.None);

        var validator = new SerializationValidator(workspace.Services);

        using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
        var syncObject = await GetRequiredAssetAsync(scope, scope.SolutionChecksum);
        var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(syncObject.Checksum);
        var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);

        await validator.VerifySynchronizationObjectInServiceAsync(syncObject);
        await validator.VerifyChecksumInServiceAsync(solutionObject.Attributes, WellKnownSynchronizationKind.SolutionAttributes);

        Assert.Equal(2, solutionObject.Projects.Length);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Full_Serialization()
    {
        using var workspace = CreateWorkspace();
        var solution = SetFullSolution(workspace);

        var validator = new SerializationValidator(workspace.Services);

        using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
        await validator.VerifySolutionStateSerializationAsync(solution, scope.SolutionChecksum);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Full_Asset_Serialization()
    {
        using var workspace = CreateWorkspace();
        var solution = SetFullSolution(workspace);

        var validator = new SerializationValidator(workspace.Services);

        using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
        var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope.SolutionChecksum);
        var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);
        await validator.VerifyAssetAsync(solutionObject);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Full_Asset_Serialization_Desktop()
    {
        using var workspace = CreateWorkspace();
        var solution = SetFullSolution(workspace);

        var validator = new SerializationValidator(workspace.Services);

        using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
        var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope.SolutionChecksum);
        var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);
        await validator.VerifyAssetAsync(solutionObject);
    }

    [Fact]
    public async Task CreateSolutionSnapshotId_Duplicate()
    {
        using var workspace = CreateWorkspace();
        var solution = SetFullSolution(workspace);

        // this is just data, one can hold the id outside of using statement. but
        // one can't get asset using checksum from the id.
        SolutionCompilationStateChecksums solutionCompilationId1;
        SolutionCompilationStateChecksums solutionCompilationId2;
        SolutionStateChecksums solutionId1;
        SolutionStateChecksums solutionId2;

        var validator = new SerializationValidator(workspace.Services);

        using (var scope1 = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None).ConfigureAwait(false))
        {
            solutionCompilationId1 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope1.SolutionChecksum);
            solutionId1 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationId1.SolutionState);
        }

        using (var scope2 = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None).ConfigureAwait(false))
        {
            solutionCompilationId2 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope2.SolutionChecksum);
            solutionId2 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationId2.SolutionState);
        }

        validator.SolutionCompilationStateEqual(solutionCompilationId1, solutionCompilationId2);

        // once pinned snapshot scope is released, there is no way to get back to asset.
        // catch Exception because it will throw 2 different exception based on release or debug (ExceptionUtilities.UnexpectedValue)
        Assert.ThrowsAny<Exception>(() => validator.SolutionStateEqual(solutionId1, solutionId2));
    }

    [Fact]
    public void MetadataReference_RoundTrip_Test()
    {
        using var workspace = CreateWorkspace();

        var reference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);

        var serializer = workspace.Services.GetService<ISerializerService>();
        var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);

        var assetFromStorage = CloneAsset(serializer, assetFromFile);
        _ = CloneAsset(serializer, assetFromStorage);
    }

    [Fact]
    public async Task Workspace_RoundTrip_Test()
    {
        using var workspace = CreateWorkspace();
        var solution = SetFullSolution(workspace);

        var validator = new SerializationValidator(workspace.Services);

        var scope1 = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);

        // recover solution from given snapshot
        var recovered = await validator.GetSolutionAsync(scope1);
        var solutionCompilationObject1 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope1.SolutionChecksum);
        var solutionObject1 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject1.SolutionState);

        // create new snapshot from recovered solution
        using var scope2 = await validator.AssetStorage.StoreAssetsAsync(recovered, CancellationToken.None);

        // verify asset created by recovered solution is good
        var solutionCompilationObject2 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope2.SolutionChecksum);
        var solutionObject2 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject2.SolutionState);
        await validator.VerifyAssetAsync(solutionObject2);

        // verify snapshots created from original solution and recovered solution are same
        validator.SolutionStateEqual(solutionObject1, solutionObject2);

        // recover new solution from recovered solution
        var roundtrip = await validator.GetSolutionAsync(scope2);

        // create new snapshot from round tripped solution
        using var scope3 = await validator.AssetStorage.StoreAssetsAsync(roundtrip, CancellationToken.None);
        // verify asset created by round trip solution is good
        var solutionCompilationObject3 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope3.SolutionChecksum);
        var solutionObject3 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject3.SolutionState);
        await validator.VerifyAssetAsync(solutionObject3);

        // verify snapshots created from original solution and round trip solution are same.
        validator.SolutionStateEqual(solutionObject2, solutionObject3);
    }

    [Fact]
    public async Task Workspace_RoundTrip_Test_Desktop()
    {
        using var workspace = CreateWorkspace();
        var solution = SetFullSolution(workspace);

        var validator = new SerializationValidator(workspace.Services);

        var scope1 = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);

        // recover solution from given snapshot
        var recovered = await validator.GetSolutionAsync(scope1);
        var solutionCompilationObject1 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope1.SolutionChecksum);
        var solutionObject1 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject1.SolutionState);

        // create new snapshot from recovered solution
        using var scope2 = await validator.AssetStorage.StoreAssetsAsync(recovered, CancellationToken.None);

        // verify asset created by recovered solution is good
        var solutionCompilationObject2 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope2.SolutionChecksum);
        var solutionObject2 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject2.SolutionState);
        await validator.VerifyAssetAsync(solutionObject2);

        // verify snapshots created from original solution and recovered solution are same
        validator.SolutionStateEqual(solutionObject1, solutionObject2);
        scope1.Dispose();

        // recover new solution from recovered solution
        var roundtrip = await validator.GetSolutionAsync(scope2);

        // create new snapshot from round tripped solution
        using var scope3 = await validator.AssetStorage.StoreAssetsAsync(roundtrip, CancellationToken.None);
        // verify asset created by round trip solution is good
        var solutionCompilationObject3 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope3.SolutionChecksum);
        var solutionObject3 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject3.SolutionState);
        await validator.VerifyAssetAsync(solutionObject3);

        // verify snapshots created from original solution and round trip solution are same.
        validator.SolutionStateEqual(solutionObject2, solutionObject3);
    }

    [Fact]
    public void Missing_Metadata_Serialization_Test()
    {
        using var workspace = CreateWorkspace();
        var serializer = workspace.Services.GetService<ISerializerService>();

        var reference = new MissingMetadataReference();

        // make sure this doesn't throw
        var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
        var assetFromStorage = CloneAsset(serializer, assetFromFile);
        _ = CloneAsset(serializer, assetFromStorage);
    }

    [Fact]
    public void Missing_Analyzer_Serialization_Test()
    {
        using var workspace = CreateWorkspace();
        var serializer = workspace.Services.GetService<ISerializerService>();

        var reference = new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing_reference"), new MissingAnalyzerLoader());

        // make sure this doesn't throw
        var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
        var assetFromStorage = CloneAsset(serializer, assetFromFile);
        _ = CloneAsset(serializer, assetFromStorage);
    }

    [Fact]
    public void Missing_Analyzer_Serialization_Desktop_Test()
    {
        using var workspace = CreateWorkspace();
        var serializer = workspace.Services.GetService<ISerializerService>();

        var reference = new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing_reference"), new MissingAnalyzerLoader());

        // make sure this doesn't throw
        var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
        var assetFromStorage = CloneAsset(serializer, assetFromFile);
        _ = CloneAsset(serializer, assetFromStorage);
    }

    [Fact]
    public void RoundTrip_Analyzer_Serialization_Test()
    {
        using var tempRoot = new TempRoot();
        using var workspace = CreateWorkspace();

        var serializer = workspace.Services.GetService<ISerializerService>();

        // actually shadow copy content
        var location = typeof(object).Assembly.Location;
        var file = tempRoot.CreateFile("shadow", "dll");
        file.CopyContentFrom(location);

        var reference = new AnalyzerFileReference(location, new MockShadowCopyAnalyzerAssemblyLoader(ImmutableDictionary<string, string>.Empty.Add(location, file.Path)));

        // make sure this doesn't throw
        var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
        var assetFromStorage = CloneAsset(serializer, assetFromFile);
        _ = CloneAsset(serializer, assetFromStorage);
    }

    [Fact]
    public void RoundTrip_Analyzer_Serialization_Desktop_Test()
    {
        using var tempRoot = new TempRoot();

        using var workspace = CreateWorkspace();
        var serializer = workspace.Services.GetService<ISerializerService>();

        // actually shadow copy content
        var location = typeof(object).Assembly.Location;
        var file = tempRoot.CreateFile("shadow", "dll");
        file.CopyContentFrom(location);

        var reference = new AnalyzerFileReference(location, new MockShadowCopyAnalyzerAssemblyLoader(ImmutableDictionary<string, string>.Empty.Add(location, file.Path)));

        // make sure this doesn't throw
        var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
        var assetFromStorage = CloneAsset(serializer, assetFromFile);
        _ = CloneAsset(serializer, assetFromStorage);
    }

    [Fact]
    public void ShadowCopied_Analyzer_Serialization_Desktop_Test()
    {
        using var tempRoot = new TempRoot();
        using var workspace = CreateWorkspace();
        var reference = CreateShadowCopiedAnalyzerReference(tempRoot);

        var serializer = workspace.Services.GetService<ISerializerService>();

        // make sure this doesn't throw
        var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);

        // this will verify serialized analyzer reference return same checksum as the original one
        _ = CloneAsset(serializer, assetFromFile);
    }

    [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1107294")]
    public async Task SnapshotWithIdenticalAnalyzerFiles()
    {
        using var workspace = CreateWorkspace();
        var project = workspace.CurrentSolution.AddProject("Project", "Project.dll", LanguageNames.CSharp);

        using var temp = new TempRoot();
        var dir = temp.CreateDirectory();

        // create two analyzer assembly files whose content is identical but path is different:
        var file1 = dir.CreateFile("analyzer1.dll").CopyContentFrom(_testFixture.FaultyAnalyzer);
        var file2 = dir.CreateFile("analyzer2.dll").CopyContentFrom(_testFixture.FaultyAnalyzer);

        var analyzer1 = new AnalyzerFileReference(file1.Path, TestAnalyzerAssemblyLoader.LoadNotImplemented);
        var analyzer2 = new AnalyzerFileReference(file2.Path, TestAnalyzerAssemblyLoader.LoadNotImplemented);

        project = project.AddAnalyzerReferences([analyzer1, analyzer2]);

        var validator = new SerializationValidator(workspace.Services);
        using var snapshot = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);

        var recovered = await validator.GetSolutionAsync(snapshot);
        AssertEx.Equal([file1.Path, file2.Path], recovered.GetProject(project.Id).AnalyzerReferences.Select(r => r.FullPath));
    }

    [Fact]
    public async Task SnapshotWithMissingReferencesTest()
    {
        using var workspace = CreateWorkspace();
        var project = workspace.CurrentSolution.AddProject("Project", "Project.dll", LanguageNames.CSharp);

        var metadata = new MissingMetadataReference();
        var analyzer = new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing_reference"), new MissingAnalyzerLoader());

        project = project.AddMetadataReference(metadata);
        project = project.AddAnalyzerReference(analyzer);

        var validator = new SerializationValidator(workspace.Services);

        using var snapshot = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);
        // this shouldn't throw
        var recovered = await validator.GetSolutionAsync(snapshot);
    }

    [Fact]
    public async Task UnknownLanguageTest()
    {
        using var workspace = CreateWorkspace([typeof(NoCompilationLanguageService)]);
        var project = workspace.CurrentSolution.AddProject("Project", "Project.dll", NoCompilationConstants.LanguageName);

        var validator = new SerializationValidator(workspace.Services);

        using var snapshot = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);
        // this shouldn't throw
        var recovered = await validator.GetSolutionAsync(snapshot);
    }

    [Fact]
    public async Task EmptyAssetChecksumTest()
    {
        var document = CreateWorkspace().CurrentSolution.AddProject("empty", "empty", LanguageNames.CSharp).AddDocument("empty", SourceText.From(""));
        var serializer = document.Project.Solution.Services.GetService<ISerializerService>();

        var text = await document.GetTextAsync().ConfigureAwait(false);
        var source = new SerializableSourceText(text, text.GetContentHash()).ContentChecksum;
        var metadata = serializer.CreateChecksum(new MissingMetadataReference(), CancellationToken.None);
        var analyzer = serializer.CreateChecksum(new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing"), new MissingAnalyzerLoader()), CancellationToken.None);

        Assert.NotEqual(source, metadata);
        Assert.NotEqual(source, analyzer);
        Assert.NotEqual(metadata, analyzer);
    }

    [Fact]
    public async Task VBParseOptionsInCompilationOptions()
    {
        var project = CreateWorkspace().CurrentSolution.AddProject("empty", "empty", LanguageNames.VisualBasic);
        project = project.WithCompilationOptions(
            ((VisualBasic.VisualBasicCompilationOptions)project.CompilationOptions).WithParseOptions((VisualBasic.VisualBasicParseOptions)project.ParseOptions));

        var checksum = await project.State.GetChecksumAsync(CancellationToken.None);

        Assert.True(checksum != Checksum.Null);
    }

    [Fact]
    public async Task TestMetadataXmlDocComment()
    {
        using var tempRoot = new TempRoot();
        // get original assembly location
        var mscorlibLocation = typeof(object).Assembly.Location;

        // set up dll and xml doc content
        var tempDir = tempRoot.CreateDirectory();
        var tempCorlib = tempDir.CopyFile(mscorlibLocation);
        var tempCorlibXml = tempDir.CreateFile(Path.ChangeExtension(tempCorlib.Path, "xml"));
        tempCorlibXml.WriteAllText("""
            <?xml version="1.0" encoding="utf-8"?>
            <doc>
              <assembly>
                <name>mscorlib</name>
              </assembly>
              <members>
                <member name="T:System.Object">
                  <summary>Supports all classes in the .NET Framework class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all classes in the .NET Framework; it is the root of the type hierarchy.To browse the .NET Framework source code for this type, see the Reference Source.</summary>
                </member>
              </members>
            </doc>
            """);

        using var workspace = CreateWorkspace();
        var solution = workspace.CurrentSolution
            .AddProject("Project", "Project.dll", LanguageNames.CSharp)
            .AddMetadataReference(MetadataReference.CreateFromFile(tempCorlib.Path))
            .Solution;

        var validator = new SerializationValidator(workspace.Services);

        using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
        // recover solution from given snapshot
        var recovered = await validator.GetSolutionAsync(scope);

        var compilation = await recovered.Projects.First().GetCompilationAsync(CancellationToken.None);
        var objectType = compilation.GetTypeByMetadataName("System.Object");
        var xmlDocComment = objectType.GetDocumentationCommentXml();

        Assert.False(string.IsNullOrEmpty(xmlDocComment));
    }

    [Fact]
    public void TestEncodingSerialization()
    {
        using var workspace = CreateWorkspace();

        var serializer = workspace.Services.GetService<ISerializerService>();

        // test with right serializable encoding
        var sourceText = SourceText.From("Hello", Encoding.UTF8);
        var serializableSourceText = new SerializableSourceText(sourceText, sourceText.GetContentHash());
        using (var stream = SerializableBytes.CreateWritableStream())
        {
            using (var objectWriter = new ObjectWriter(stream, leaveOpen: true))
            {
                serializer.Serialize(serializableSourceText, objectWriter, CancellationToken.None);
            }

            stream.Position = 0;

            using var objectReader = ObjectReader.TryGetReader(stream);

            var newText = (SerializableSourceText)serializer.Deserialize(serializableSourceText.GetWellKnownSynchronizationKind(), objectReader, CancellationToken.None);
            Assert.Equal(sourceText.ToString(), newText.GetText(CancellationToken.None).ToString());
        }

        // test with wrong encoding that doesn't support serialization
        sourceText = SourceText.From("Hello", new NotSerializableEncoding());
        serializableSourceText = new SerializableSourceText(sourceText, sourceText.GetContentHash());
        using (var stream = SerializableBytes.CreateWritableStream())
        {
            using (var objectWriter = new ObjectWriter(stream, leaveOpen: true))
            {
                serializer.Serialize(serializableSourceText, objectWriter, CancellationToken.None);
            }

            stream.Position = 0;

            using var objectReader = ObjectReader.TryGetReader(stream);

            var newText = (SerializableSourceText)serializer.Deserialize(serializableSourceText.GetWellKnownSynchronizationKind(), objectReader, CancellationToken.None);
            Assert.Equal(sourceText.ToString(), newText.GetText(CancellationToken.None).ToString());
        }
    }

    [Fact]
    public void TestCompilationOptions_NullableAndImport()
    {
        var csharpOptions = CSharp.CSharpCompilation.Create("dummy").Options.WithNullableContextOptions(NullableContextOptions.Warnings).WithMetadataImportOptions(MetadataImportOptions.All);
        var vbOptions = VisualBasic.VisualBasicCompilation.Create("dummy").Options.WithMetadataImportOptions(MetadataImportOptions.Internal);

        using var workspace = CreateWorkspace();
        var serializer = workspace.Services.GetService<ISerializerService>();

        VerifyOptions(csharpOptions);
        VerifyOptions(vbOptions);

        void VerifyOptions(CompilationOptions originalOptions)
        {
            using var stream = SerializableBytes.CreateWritableStream();
            using (var objectWriter = new ObjectWriter(stream, leaveOpen: true))
            {
                serializer.Serialize(originalOptions, objectWriter, CancellationToken.None);
            }

            stream.Position = 0;
            using var objectReader = ObjectReader.TryGetReader(stream);
            var recoveredOptions = (CompilationOptions)serializer.Deserialize(originalOptions.GetWellKnownSynchronizationKind(), objectReader, CancellationToken.None);

            var original = serializer.CreateChecksum(originalOptions, CancellationToken.None);
            var recovered = serializer.CreateChecksum(recoveredOptions, CancellationToken.None);

            Assert.Equal(original, recovered);
        }
    }

    private static SolutionAsset CloneAsset(ISerializerService serializer, SolutionAsset asset)
    {
        using var stream = SerializableBytes.CreateWritableStream();
        using (var writer = new ObjectWriter(stream, leaveOpen: true))
        {
            serializer.Serialize(asset.Value, writer, CancellationToken.None);
        }

        stream.Position = 0;
        using var reader = ObjectReader.TryGetReader(stream);
        var recovered = serializer.Deserialize(asset.Kind, reader, CancellationToken.None);
        var checksum = recovered is SerializableSourceText text ? text.ContentChecksum : serializer.CreateChecksum(recovered, CancellationToken.None);

        var assetFromStorage = new SolutionAsset(checksum, recovered);

        Assert.Equal(asset.Checksum, assetFromStorage.Checksum);
        return assetFromStorage;
    }

    private static AnalyzerFileReference CreateShadowCopiedAnalyzerReference(TempRoot tempRoot)
    {
        // use 2 different files as shadow copied content
        var original = typeof(AdhocWorkspace).Assembly.Location;

        var shadow = tempRoot.CreateFile("shadow", "dll");
        shadow.CopyContentFrom(typeof(object).Assembly.Location);

        return new AnalyzerFileReference(original, new MockShadowCopyAnalyzerAssemblyLoader(ImmutableDictionary<string, string>.Empty.Add(original, shadow.Path)));
    }

    private sealed class MissingAnalyzerLoader() : IAnalyzerAssemblyLoader
    {
        public void AddDependencyLocation(string fullPath)
        {
        }

        public Assembly LoadFromPath(string fullPath)
            => throw new FileNotFoundException(fullPath);
    }

    private sealed class MissingMetadataReference : PortableExecutableReference
    {
        public MissingMetadataReference()
            : base(MetadataReferenceProperties.Assembly, "missing_reference", XmlDocumentationProvider.Default)
        {
        }

        protected override DocumentationProvider CreateDocumentationProvider()
            => null;

        protected override Metadata GetMetadataImpl()
            => throw new FileNotFoundException("can't find");

        protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties)
            => this;
    }

    private sealed class MockShadowCopyAnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
    {
        private readonly ImmutableDictionary<string, string> _map;

        public MockShadowCopyAnalyzerAssemblyLoader(ImmutableDictionary<string, string> map)
            => _map = map;

        public void AddDependencyLocation(string fullPath)
        {
        }

        public Assembly LoadFromPath(string fullPath)
            => Assembly.LoadFrom(_map[fullPath]);
    }

    private sealed class NotSerializableEncoding : Encoding
    {
        private readonly Encoding _real = Encoding.UTF8;

        public override string WebName => _real.WebName;
        public override int GetByteCount(char[] chars, int index, int count) => _real.GetByteCount(chars, index, count);
        public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) => GetBytes(chars, charIndex, charCount, bytes, byteIndex);
        public override int GetCharCount(byte[] bytes, int index, int count) => GetCharCount(bytes, index, count);
        public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) => GetChars(bytes, byteIndex, byteCount, chars, charIndex);
        public override int GetMaxByteCount(int charCount) => GetMaxByteCount(charCount);
        public override int GetMaxCharCount(int byteCount) => GetMaxCharCount(byteCount);
    }
}
