﻿// 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.Globalization;
using System.Reflection;
using System.Reflection.Metadata;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests
{
    public class AssemblyIdentityTests : AssemblyIdentityTestBase
    {
        [Fact]
        public void Equality()
        {
            var id1 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKey1, hasPublicKey: true, isRetargetable: false);
            var id11 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKey1, hasPublicKey: true, isRetargetable: false);
            var id2 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);
            var id22 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);

            var id3 = new AssemblyIdentity("Goo!", new Version(1, 0, 0, 0), "", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);
            var id4 = new AssemblyIdentity("Goo", new Version(1, 0, 1, 0), "", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);
            var id5 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "en-US", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);
            var id6 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", default(ImmutableArray<byte>), hasPublicKey: false, isRetargetable: false);
            var id7 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKey1, hasPublicKey: true, isRetargetable: true);

            var win1 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKey1, hasPublicKey: true, isRetargetable: false, contentType: AssemblyContentType.WindowsRuntime);
            var win2 = new AssemblyIdentity("Bar", new Version(1, 0, 0, 0), "", RoPublicKey1, hasPublicKey: true, isRetargetable: false, contentType: AssemblyContentType.WindowsRuntime);
            var win3 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKey1, hasPublicKey: true, isRetargetable: false, contentType: AssemblyContentType.WindowsRuntime);

            Assert.True(id1.Equals(id1));
            Assert.True(id1.Equals(id2));
            Assert.True(id2.Equals(id1));
            Assert.True(id1.Equals(id11));
            Assert.True(id11.Equals(id1));
            Assert.True(id2.Equals(id22));
            Assert.True(id22.Equals(id2));

            Assert.False(id1.Equals(id3));
            Assert.False(id1.Equals(id4));
            Assert.False(id1.Equals(id5));
            Assert.False(id1.Equals(id6));
            Assert.False(id1.Equals(id7));

            Assert.Equal((object)id1, id1);
            Assert.NotNull(id1);
            Assert.False(id2.Equals((AssemblyIdentity)null));

            Assert.Equal(id1.GetHashCode(), id2.GetHashCode());

            Assert.False(win1.Equals(win2));
            Assert.False(win1.Equals(id1));
            Assert.True(win1.Equals(win3));

            Assert.Equal(win1.GetHashCode(), win3.GetHashCode());
        }

        [Fact]
        public void Equality_InvariantCulture()
        {
            var neutral1 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "NEUtral", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);
            var neutral2 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), null, RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);
            var neutral3 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "neutral", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);
            var invariant = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);

            Assert.True(neutral1.Equals(invariant));
            Assert.True(neutral2.Equals(invariant));
            Assert.True(neutral3.Equals(invariant));
        }

        [Fact]
        public void FromAssemblyDefinitionInvalidParameters()
        {
            Assembly asm = null;
            Assert.Throws<ArgumentNullException>(() => { AssemblyIdentity.FromAssemblyDefinition(asm); });
        }

        [Fact]
        public void FromAssemblyDefinition()
        {
            var name = new AssemblyName("goo");
            name.Flags = AssemblyNameFlags.Retargetable | AssemblyNameFlags.PublicKey | AssemblyNameFlags.EnableJITcompileOptimizer | AssemblyNameFlags.EnableJITcompileTracking;
            name.CultureInfo = new CultureInfo("en-US", useUserOverride: false);
            name.ContentType = AssemblyContentType.Default;
            name.Version = new Version(1, 2, 3, 4);
#pragma warning disable SYSLIB0037
            // warning SYSLIB0037: 'AssemblyName.ProcessorArchitecture' is obsolete: 'AssemblyName members HashAlgorithm, ProcessorArchitecture, and VersionCompatibility are obsolete and not supported.'
            name.ProcessorArchitecture = ProcessorArchitecture.X86;
#pragma warning restore SYSLIB0037

            var id = AssemblyIdentity.FromAssemblyDefinition(name);
            Assert.Equal("goo", id.Name);
            Assert.True(id.IsRetargetable);
            Assert.Equal(new Version(1, 2, 3, 4), id.Version);
            Assert.Equal(AssemblyContentType.Default, id.ContentType);
            Assert.False(id.HasPublicKey);
            Assert.False(id.IsStrongName);

            name = new AssemblyName("goo");
            name.SetPublicKey(PublicKey1);
            name.Version = new Version(1, 2, 3, 4);

            id = AssemblyIdentity.FromAssemblyDefinition(name);
            Assert.Equal("goo", id.Name);
            Assert.Equal(new Version(1, 2, 3, 4), id.Version);
            Assert.True(id.HasPublicKey);
            Assert.True(id.IsStrongName);
            AssertEx.Equal(id.PublicKey, PublicKey1);

            name = new AssemblyName("goo");
            name.ContentType = AssemblyContentType.WindowsRuntime;

            id = AssemblyIdentity.FromAssemblyDefinition(name);
            Assert.Equal("goo", id.Name);
            Assert.Equal(AssemblyContentType.WindowsRuntime, id.ContentType);
        }

        [Fact]
        public void FromAssemblyDefinition_InvariantCulture()
        {
            var name = new AssemblyName("goo");
            name.Flags = AssemblyNameFlags.None;
            name.CultureInfo = CultureInfo.InvariantCulture;
            name.ContentType = AssemblyContentType.Default;
            name.Version = new Version(1, 2, 3, 4);
#pragma warning disable SYSLIB0037
            name.ProcessorArchitecture = ProcessorArchitecture.X86;
#pragma warning restore SYSLIB0037

            var id = AssemblyIdentity.FromAssemblyDefinition(name);
            Assert.Equal("", id.CultureName);
        }

        [Fact]
        public void Properties()
        {
            var id = new AssemblyIdentity("Goo", hasPublicKey: false, isRetargetable: false);
            Assert.Equal("Goo", id.Name);
            Assert.Equal(new Version(0, 0, 0, 0), id.Version);
            Assert.Equal(AssemblyNameFlags.None, id.Flags);
            Assert.Equal("", id.CultureName);
            Assert.False(id.HasPublicKey);
            Assert.False(id.IsRetargetable);
            Assert.Equal(0, id.PublicKey.Length);
            Assert.Equal(0, id.PublicKeyToken.Length);
            Assert.Equal(AssemblyContentType.Default, id.ContentType);

            id = new AssemblyIdentity("Goo", publicKeyOrToken: RoPublicKey1, hasPublicKey: true, isRetargetable: false);
            Assert.Equal("Goo", id.Name);
            Assert.Equal(new Version(0, 0, 0, 0), id.Version);
            Assert.Equal(AssemblyNameFlags.PublicKey, id.Flags);
            Assert.Equal("", id.CultureName);
            Assert.True(id.HasPublicKey);
            Assert.False(id.IsRetargetable);
            AssertEx.Equal(PublicKey1, id.PublicKey);
            AssertEx.Equal(PublicKeyToken1, id.PublicKeyToken);
            Assert.Equal(AssemblyContentType.Default, id.ContentType);

            id = new AssemblyIdentity("Goo", publicKeyOrToken: RoPublicKeyToken1, hasPublicKey: false, isRetargetable: true);
            Assert.Equal("Goo", id.Name);
            Assert.Equal(new Version(0, 0, 0, 0), id.Version);
            Assert.Equal(AssemblyNameFlags.Retargetable, id.Flags);
            Assert.Equal("", id.CultureName);
            Assert.False(id.HasPublicKey);
            Assert.True(id.IsRetargetable);
            Assert.Equal(0, id.PublicKey.Length);
            AssertEx.Equal(PublicKeyToken1, id.PublicKeyToken);
            Assert.Equal(AssemblyContentType.Default, id.ContentType);

            id = new AssemblyIdentity("Goo", publicKeyOrToken: RoPublicKey1, hasPublicKey: true, isRetargetable: true);
            Assert.Equal("Goo", id.Name);
            Assert.Equal(new Version(0, 0, 0, 0), id.Version);
            Assert.Equal(AssemblyNameFlags.PublicKey | AssemblyNameFlags.Retargetable, id.Flags);
            Assert.Equal("", id.CultureName);
            Assert.True(id.HasPublicKey);
            Assert.True(id.IsRetargetable);
            AssertEx.Equal(PublicKey1, id.PublicKey);
            AssertEx.Equal(PublicKeyToken1, id.PublicKeyToken);
            Assert.Equal(AssemblyContentType.Default, id.ContentType);

            id = new AssemblyIdentity("Goo", publicKeyOrToken: RoPublicKey1, hasPublicKey: true, contentType: AssemblyContentType.WindowsRuntime);
            Assert.Equal("Goo", id.Name);
            Assert.Equal(new Version(0, 0, 0, 0), id.Version);
            Assert.Equal(AssemblyNameFlags.PublicKey, id.Flags);
            Assert.Equal("", id.CultureName);
            Assert.True(id.HasPublicKey);
            Assert.False(id.IsRetargetable);
            AssertEx.Equal(PublicKey1, id.PublicKey);
            AssertEx.Equal(PublicKeyToken1, id.PublicKeyToken);
            Assert.Equal(AssemblyContentType.WindowsRuntime, id.ContentType);
        }

        [Fact]
        public void IsStrongName()
        {
            var id1 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKey1, hasPublicKey: true, isRetargetable: false);
            Assert.True(id1.IsStrongName);

            var id2 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false);
            Assert.True(id2.IsStrongName);

            var id3 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", ImmutableArray<byte>.Empty, hasPublicKey: false, isRetargetable: false);
            Assert.False(id3.IsStrongName);

            // for WinRT references "strong name" doesn't make sense:

            var id4 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", ImmutableArray<byte>.Empty, hasPublicKey: false, isRetargetable: false, contentType: AssemblyContentType.WindowsRuntime);
            Assert.False(id4.IsStrongName);

            var id5 = new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", RoPublicKeyToken1, hasPublicKey: false, isRetargetable: false, contentType: AssemblyContentType.WindowsRuntime);
            Assert.True(id5.IsStrongName);
        }

        [Fact]
        public void InvalidConstructorArgs()
        {
            Assert.Throws<ArgumentException>(() => new AssemblyIdentity("xx\0xx"));
            Assert.Throws<ArgumentException>(() => new AssemblyIdentity(""));
            Assert.Throws<ArgumentException>(() => new AssemblyIdentity(null));

            Assert.Throws<ArgumentException>(
                () => new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", ImmutableArray<byte>.Empty, hasPublicKey: true, isRetargetable: false, contentType: AssemblyContentType.Default));

            Assert.Throws<ArgumentException>(
                () => new AssemblyIdentity("Goo", new Version(1, 0, 0, 0), "", new byte[] { 1, 2, 3 }.AsImmutableOrNull(), hasPublicKey: false, isRetargetable: false, contentType: AssemblyContentType.Default));

            foreach (var v in new Version[]
            {
                new Version(),
                new Version(0, 0),
                new Version(0, 0, 0),
                new Version(int.MaxValue, 0, 0, 0),
                new Version(0, int.MaxValue, 0, 0),
                new Version(0, 0, int.MaxValue, 0),
                new Version(0, 0, 0, int.MaxValue),
            })
            {
                Assert.Throws<ArgumentOutOfRangeException>(() => new AssemblyIdentity("Goo", v));
            }

            Assert.Throws<ArgumentOutOfRangeException>(() => new AssemblyIdentity("Goo", contentType: (AssemblyContentType)(-1)));
            Assert.Throws<ArgumentOutOfRangeException>(() => new AssemblyIdentity("Goo", contentType: (AssemblyContentType)int.MaxValue));

            Assert.Throws<ArgumentException>(() =>
                new AssemblyIdentity("Goo", publicKeyOrToken: RoPublicKey1, hasPublicKey: true, isRetargetable: true, contentType: AssemblyContentType.WindowsRuntime));
        }

        [Fact]
        public void MetadataConstructor()
        {
            var id = new AssemblyIdentity(/*noThrow:*/true, "Goo", new Version(1, 2, 3, 4), "en-US", RoPublicKey1,
                hasPublicKey: true, isRetargetable: true, contentType: AssemblyContentType.Default);
            Assert.Equal("Goo", id.Name);
            Assert.Equal(new Version(1, 2, 3, 4), id.Version);
            Assert.Equal(AssemblyNameFlags.PublicKey | AssemblyNameFlags.Retargetable, id.Flags);
            Assert.Equal("en-US", id.CultureName);
            Assert.True(id.HasPublicKey);
            Assert.True(id.IsRetargetable);
            AssertEx.Equal(PublicKey1, id.PublicKey);
            AssertEx.Equal(PublicKeyToken1, id.PublicKeyToken);
            Assert.Equal(AssemblyContentType.Default, id.ContentType);

            // invalid content type:
            id = new AssemblyIdentity(/*noThrow:*/true, "Goo", new Version(1, 2, 3, 4), null, ImmutableArray<byte>.Empty,
                hasPublicKey: false, isRetargetable: false, contentType: (AssemblyContentType)2);
            Assert.Equal(AssemblyNameFlags.None, id.Flags);
            Assert.Equal("", id.CultureName);
            Assert.False(id.HasPublicKey);
            Assert.Equal(0, id.PublicKey.Length);
            Assert.Equal(0, id.PublicKeyToken.Length);
            Assert.Equal(AssemblyContentType.Default, id.ContentType);

            // default Retargetable=No if content type is WinRT
            id = new AssemblyIdentity(/*noThrow:*/true, "Goo", new Version(1, 2, 3, 4), null, ImmutableArray<byte>.Empty,
                hasPublicKey: false, isRetargetable: true, contentType: AssemblyContentType.WindowsRuntime);
            Assert.Equal("Goo", id.Name);
            Assert.Equal(new Version(1, 2, 3, 4), id.Version);
            Assert.Equal(AssemblyNameFlags.None, id.Flags);
            Assert.Equal("", id.CultureName);
            Assert.False(id.HasPublicKey);
            Assert.False(id.IsRetargetable);
            Assert.Equal(AssemblyContentType.WindowsRuntime, id.ContentType);

            // invalid culture:
            // The native compiler doesn't enforce that the culture be anything in particular. 
            // AssemblyIdentity should preserve user input even if it is of dubious utility.
            id = new AssemblyIdentity(/*noThrow:*/true, "Goo", new Version(1, 2, 3, 4), "blah,", ImmutableArray<byte>.Empty,
                hasPublicKey: false, isRetargetable: false, contentType: AssemblyContentType.Default);
            Assert.Equal("blah,", id.CultureName);

            id = new AssemblyIdentity(/*noThrow:*/true, "Goo", new Version(1, 2, 3, 4), "*", ImmutableArray<byte>.Empty,
                hasPublicKey: false, isRetargetable: false, contentType: AssemblyContentType.Default);
            Assert.Equal("*", id.CultureName);

            id = new AssemblyIdentity(/*noThrow:*/true, "Goo", new Version(1, 2, 3, 4), "neutral", ImmutableArray<byte>.Empty,
                hasPublicKey: false, isRetargetable: false, contentType: AssemblyContentType.Default);
            Assert.Equal("", id.CultureName);
        }

        [Fact]
        public void ToAssemblyName()
        {
            var ai = new AssemblyIdentity("goo");
            var an = ai.ToAssemblyName();
            Assert.Equal("goo", an.Name);
            Assert.Equal(new Version(0, 0, 0, 0), an.Version);
            Assert.Equal(CultureInfo.InvariantCulture, an.CultureInfo);
            AssertEx.Equal(new byte[0], an.GetPublicKeyToken());
            AssertEx.Equal(null, an.GetPublicKey());
            Assert.Equal(AssemblyNameFlags.None, an.Flags);
#pragma warning disable SYSLIB0044
            // warning SYSLIB0044: 'AssemblyName.CodeBase' is obsolete: 'AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete. Using them for loading an assembly is not supported.'
            Assert.Null(an.CodeBase);
#pragma warning restore SYSLIB0044

            ai = new AssemblyIdentity("goo", new Version(1, 2, 3, 4), "en-US", RoPublicKey1,
                hasPublicKey: true,
                isRetargetable: true);

            an = ai.ToAssemblyName();
            Assert.Equal("goo", an.Name);
            Assert.Equal(new Version(1, 2, 3, 4), an.Version);
            Assert.Equal(CultureInfo.GetCultureInfo("en-US"), an.CultureInfo);
            AssertEx.Equal(PublicKeyToken1, an.GetPublicKeyToken());
            AssertEx.Equal(PublicKey1, an.GetPublicKey());
            Assert.Equal(AssemblyNameFlags.PublicKey | AssemblyNameFlags.Retargetable, an.Flags);
#pragma warning disable SYSLIB0044
            Assert.Null(an.CodeBase);
#pragma warning restore SYSLIB0044

            // invalid characters are ok in the name, the FullName can't be built though:
            foreach (char c in ClrInvalidCharacters)
            {
                ai = new AssemblyIdentity(c.ToString());
                an = ai.ToAssemblyName();

                Assert.Equal(c.ToString(), an.Name);
                Assert.Equal(new Version(0, 0, 0, 0), an.Version);
                Assert.Equal(CultureInfo.InvariantCulture, an.CultureInfo);
                AssertEx.Equal(new byte[0], an.GetPublicKeyToken());
                AssertEx.Equal(null, an.GetPublicKey());
                Assert.Equal(AssemblyNameFlags.None, an.Flags);
#pragma warning disable SYSLIB0044
                AssertEx.Equal(null, an.CodeBase);
#pragma warning restore SYSLIB0044
            }
        }

        [ConditionalFact(typeof(DesktopOnly), Reason = ConditionalSkipReason.TestExecutionNeedsDesktopTypes)]
        public void ToAssemblyName_Errors()
        {
            var ai = new AssemblyIdentity("goo", cultureName: "*");
            Assert.Throws<CultureNotFoundException>(() => ai.ToAssemblyName());
        }

        [Fact]
        public void Keys()
        {
            var an = new AssemblyName();
            an.Name = "Goo";
            an.Version = new Version(1, 0, 0, 0);
            an.SetPublicKey(PublicKey1);
            var anPkt = an.GetPublicKeyToken();
            var aiPkt = AssemblyIdentity.CalculatePublicKeyToken(RoPublicKey1);
            AssertEx.Equal(PublicKeyToken1, anPkt);
            AssertEx.Equal(PublicKeyToken1, aiPkt);
        }

        [ConditionalFact(typeof(WindowsDesktopOnly), Reason = ConditionalSkipReason.TestExecutionNeedsFusion)]
        public void FullKeyAndToken()
        {
            string displayPkt = "Goo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=" + StrPublicKeyToken1;
            string displayPk = "Goo, Version=1.0.0.0, Culture=neutral, PublicKey=" + StrPublicKey1;

            bool equivalent;
            FusionAssemblyIdentityComparer.AssemblyComparisonResult result;
            int hr = FusionAssemblyIdentityComparer.DefaultModelCompareAssemblyIdentity(displayPkt, false, displayPk, false, out equivalent, out result, IntPtr.Zero);

            Assert.Equal(0, hr);
            Assert.True(equivalent, "Expected equivalent");
            Assert.Equal(FusionAssemblyIdentityComparer.AssemblyComparisonResult.EquivalentFullMatch, result);

            hr = FusionAssemblyIdentityComparer.DefaultModelCompareAssemblyIdentity(displayPk, false, displayPkt, false, out equivalent, out result, IntPtr.Zero);

            Assert.Equal(0, hr);
            Assert.True(equivalent, "Expected equivalent");
            Assert.Equal(FusionAssemblyIdentityComparer.AssemblyComparisonResult.EquivalentFullMatch, result);
        }
    }
}

