using System;
using System.Collections.Generic;
using System.Globalization;
using Xunit;
using Xunit.Extensions;

namespace NuGet.Test
{
    public class SemanticVersionTest
    {
        [Theory]
        [PropertyData("ConstructorData")]
        public void StringConstructorParsesValuesCorrectly(string version, Version versionValue, string specialValue)
        {
            // Act
            SemanticVersion semanticVersion = new SemanticVersion(version);

            // Assert
            Assert.Equal(versionValue, semanticVersion.Version);
            Assert.Equal(specialValue, semanticVersion.SpecialVersion);
            Assert.Equal(version, semanticVersion.ToString());
        }

        public static IEnumerable<object[]> ConstructorData
        {
            get
            {
                yield return new object[] { "1.0.0", new Version("1.0.0.0"), "" };
                yield return new object[] { "2.3-alpha", new Version("2.3.0.0"), "alpha" };
                yield return new object[] { "3.4.0.3-RC-3", new Version("3.4.0.3"), "RC-3" };
            }
        }

        [Fact]
        public void ParseThrowsIfStringIsNullOrEmpty()
        {
            ExceptionAssert.ThrowsArgNullOrEmpty(() => SemanticVersion.Parse(version: null), "version");
            ExceptionAssert.ThrowsArgNullOrEmpty(() => SemanticVersion.Parse(version: String.Empty), "version");
        }

        [Theory]
        [InlineData("1")]
        [InlineData("1beta")]
        [InlineData("1.2Av^c")]
        [InlineData("1.2..")]
        [InlineData("1.2.3.4.5")]
        [InlineData("1.2.3.Beta")]
        [InlineData("1.2.3.4This version is full of awesomeness!!")]
        [InlineData("So.is.this")]
        [InlineData("1.34.2Alpha")]
        [InlineData("1.34.2Release Candidate")]
        [InlineData("1.4.7-")]
        [InlineData("1.4.7-.")]
        [InlineData("1.4.7-+")]
        [InlineData("1.4.7-*")]
        [InlineData("1.4.7+*")]
        [InlineData("1.4.7-AA.01^")]
        [InlineData("1.4.7-AA.0A^")]
        [InlineData("1.4.7-A^A")]
        [InlineData("1.4.7+AA.01^")]
        public void ParseThrowsIfStringIsNotAValidSemVer(string versionString)
        {
            ExceptionAssert.ThrowsArgumentException(() => SemanticVersion.Parse(versionString),
                "version",
                String.Format(CultureInfo.InvariantCulture, "'{0}' is not a valid version string.", versionString));
        }

        public static IEnumerable<object[]> LegacyVersionData
        {
            get
            {
                yield return new object[] { "1.022", new SemanticVersion(new Version("1.22.0.0"), "") };
                yield return new object[] { "23.2.3", new SemanticVersion(new Version("23.2.3.0"), "") };
                yield return new object[] { "1.3.42.10133", new SemanticVersion(new Version("1.3.42.10133"), "") };
            }
        }

        [Theory]
        [PropertyData("LegacyVersionData")]
        public void ParseReadsLegacyStyleVersionNumbers(string versionString, SemanticVersion expected)
        {
            // Act
            var actual = SemanticVersion.Parse(versionString);

            // Assert
            Assert.Equal(expected.Version, actual.Version);
            Assert.Equal(expected.SpecialVersion, actual.SpecialVersion);
        }

        public static IEnumerable<object[]> SemVerData
        {
            get
            {
                yield return new object[] { "1.022-Beta", new SemanticVersion(new Version("1.22.0.0"), "Beta") };
                yield return new object[] { "23.2.3-Alpha", new SemanticVersion(new Version("23.2.3.0"), "Alpha") };
                yield return new object[] { "1.3.42.10133-PreRelease", new SemanticVersion(new Version("1.3.42.10133"), "PreRelease") };
                yield return new object[] { "1.3.42.200930-RC-2", new SemanticVersion(new Version("1.3.42.200930"), "RC-2") };
                yield return new object[] { "1.3.42.200930-RC.2", new SemanticVersion(new Version("1.3.42.200930"), "RC.2") };
                yield return new object[] { "1.3.42.200930-RC.2", new SemanticVersion(new Version("1.3.42.200930"), "RC.2", "meta.data.1.2-a") };
                yield return new object[] { "23.2.3+Alpha", new SemanticVersion(new Version("23.2.3.0"), null, "Alpha") };
                yield return new object[] { "1.0.0--", new SemanticVersion(new Version("1.0.0"), "-") };
                yield return new object[] { "1.0.0+-", new SemanticVersion(new Version("1.0.0"), null, "-") };
            }
        }

        [Theory]
        [PropertyData("SemVerData")]
        public void ParseReadsSemverAndHybridSemverVersionNumbers(string versionString, SemanticVersion expected)
        {
            // Act
            var actual = SemanticVersion.Parse(versionString);

            // Assert
            Assert.Equal(expected.Version, actual.Version);
            Assert.Equal(expected.SpecialVersion, actual.SpecialVersion);
        }

        public static IEnumerable<object[]> SemVerWithWhiteSpace
        {
            get
            {
                yield return new object[] { "  1.022-Beta", new SemanticVersion(new Version("1.22.0.0"), "Beta") };
                yield return new object[] { "23.2.3-Alpha  ", new SemanticVersion(new Version("23.2.3.0"), "Alpha") };
                yield return new object[] { "    1.3.42.10133-PreRelease  ", new SemanticVersion(new Version("1.3.42.10133"), "PreRelease") };
                yield return new object[] { "23.2.3-1.2  ", new SemanticVersion(new Version("23.2.3.0"), "1.2", "Alpha") };
            }
        }

        [Theory]
        [PropertyData("SemVerWithWhiteSpace")]
        public void ParseIgnoresLeadingAndTrailingWhitespace(string versionString, SemanticVersion expected)
        {
            // Act
            var actual = SemanticVersion.Parse(versionString);

            // Assert
            Assert.Equal(expected.Version, actual.Version);
            Assert.Equal(expected.SpecialVersion, actual.SpecialVersion);
        }

        [Theory]
        [InlineData("1.0", "1.0.1")]
        [InlineData("1.23", "1.231")]
        [InlineData("1.4.5.6", "1.45.6")]
        [InlineData("1.4.5.6", "1.4.5.60")]
        [InlineData("1.01", "1.10")]
        [InlineData("1.01-alpha", "1.10-beta")]
        [InlineData("1.01.0-RC-1", "1.10.0-rc-2")]
        [InlineData("1.01-RC-1", "1.01")]
        [InlineData("1.01", "1.2-preview")]
        [InlineData("1.01-9", "1.01-10")]
        public void SemVerLessThanAndGreaterThanOperatorsWorks(string versionA, string versionB)
        {
            // Arrange
            var itemA = new SemanticVersion(versionA);
            var itemB = new SemanticVersion(versionB);
            object objectB = itemB;

            // Act and Assert
            Assert.True(itemA < itemB);
            Assert.True(itemA <= itemB);
            Assert.True(itemB > itemA);
            Assert.True(itemB >= itemA);
            Assert.False(itemA.Equals(itemB));
            Assert.False(itemA.Equals(objectB));
        }

        [Theory]
        [InlineData(new object[] { 1 })]
        [InlineData(new object[] { "1.0.0" })]
        [InlineData(new object[] { new object[0] })]
        public void EqualsReturnsFalseIfComparingANonSemVerType(object other)
        {
            // Arrange
            var semVer = new SemanticVersion("1.0.0");

            // Act and Assert
            Assert.False(semVer.Equals(other));
        }

        [Fact]
        public void SemVerThrowsIfLeftHandExpressionForCompareOperatorsIsNull()
        {
            // Arrange
            SemanticVersion itemA = null;
            SemanticVersion itemB = new SemanticVersion("1.0");

            // Disable this warning since it complains on mono
#pragma warning disable 0219
            // Act and Assert
            ExceptionAssert.ThrowsArgNull(() => { bool val = itemA < itemB; }, "version1");
            ExceptionAssert.ThrowsArgNull(() => { bool val = itemA <= itemB; }, "version1");
            ExceptionAssert.ThrowsArgNull(() => { bool val = itemA > itemB; }, "version1");
            ExceptionAssert.ThrowsArgNull(() => { bool val = itemA >= itemB; }, "version1");
#pragma warning restore 0219

        }

        [Theory]
        [InlineData("1.0", "1.0.0.0")]
        [InlineData("1.23.01", "1.23.1")]
        [InlineData("1.45.6", "1.45.6.0")]
        [InlineData("1.45.6-Alpha", "1.45.6-Alpha")]
        [InlineData("1.6.2-BeTa", "1.6.02-beta")]
        [InlineData("22.3.07     ", "22.3.07")]
        [InlineData("1.0+a", "1.0.0.0+a")]
        [InlineData("1.0-1.1+a", "1.0.0.0-1.1+a")]
        public void SemVerEqualsOperatorWorks(string versionA, string versionB)
        {
            // Arrange
            var itemA = new SemanticVersion(versionA);
            var itemB = new SemanticVersion(versionB);
            object objectB = itemB;

            // Act and Assert
            Assert.True(itemA == itemB);
            Assert.True(itemA.Equals(itemB));
            Assert.True(itemA.Equals(objectB));
            Assert.True(itemA <= itemB);
            Assert.True(itemB == itemA);
            Assert.True(itemB >= itemA);
        }

        [Fact]
        public void SemVerEqualityComparisonsWorkForNullValues()
        {
            // Arrange
            SemanticVersion itemA = null;
            SemanticVersion itemB = null;

            // Act and Assert
            Assert.True(itemA == itemB);
            Assert.True(itemB == itemA);
            Assert.True(itemA <= itemB);
            Assert.True(itemB <= itemA);
            Assert.True(itemA >= itemB);
            Assert.True(itemB >= itemA);
        }

        [Theory]
        [InlineData("1.0")]
        [InlineData("1.0.0")]
        [InlineData("1.0.0.0")]
        [InlineData("1.0-alpha")]
        [InlineData("1.0.0-b")]
        [InlineData("3.0.1.2")]
        [InlineData("2.1.4.3-pre-1")]
        public void ToStringReturnsOriginalValue(string version)
        {
            // Act
            SemanticVersion semVer = new SemanticVersion(version);

            // Assert
            Assert.Equal(version, semVer.ToString());
            Assert.Equal(version, semVer.ToOriginalString());
        }

        [Theory]
        [InlineData("1.0.0+meta")]
        [InlineData("1.0.0+meta.1-2")]
        [InlineData("1.0.0.0+meta.1-2")]
        [InlineData("1.0.0.0-1.2+meta.1-2")]
        public void ToStringReturnsDropsMetadata(string version)
        {
            // Act
            SemanticVersion semVer = new SemanticVersion(version);

            // Assert
            Assert.Equal(-1, semVer.ToString().IndexOf('+'));
            Assert.Equal(-1, semVer.ToNormalizedString().IndexOf('+'));
        }

        public static IEnumerable<object[]> ToStringFromVersionData
        {
            get
            {
                yield return new object[] { new Version("1.0"), null, "1.0" };
                yield return new object[] { new Version("1.0.3.120"), String.Empty, "1.0.3.120" };
                yield return new object[] { new Version("1.0.3.120"), "alpha", "1.0.3.120-alpha" };
                yield return new object[] { new Version("1.0.3.120"), "rc-2", "1.0.3.120-rc-2" };
                yield return new object[] { new Version("1.0.3.120"), "rc.2", "1.0.3.120-rc.2" };
            }
        }

        [Theory]
        [PropertyDataAttribute("ToStringFromVersionData")]
        public void ToStringConstructedFromVersionAndSpecialVersionConstructor(Version version, string specialVersion, string expected)
        {
            // Act
            SemanticVersion semVer = new SemanticVersion(version, specialVersion);

            // Assert
            Assert.Equal(expected, semVer.ToString());
        }

        [Fact]
        public void TryParseStrictParsesStrictVersion()
        {
            // Arrange
            var versionString = "1.3.2-CTP-2-Refresh-Alpha";

            // Act
            SemanticVersion version;
            bool result = SemanticVersion.TryParseStrict(versionString, out version);

            // Assert
            Assert.True(result);
            Assert.Equal(new Version("1.3.2.0"), version.Version);
            Assert.Equal("CTP-2-Refresh-Alpha", version.SpecialVersion);
        }

        [Theory]
        [InlineData("2.7")]
        [InlineData("1.3.4.5")]
        [InlineData("1.3-alpha")]
        [InlineData("1.3 .4")]
        [InlineData("2.3.18.2-a")]
        public void TryParseStrictReturnsFalseIfVersionIsNotStrictSemVer(string version)
        {
            // Act 
            SemanticVersion semanticVersion;
            bool result = SemanticVersion.TryParseStrict(version, out semanticVersion);

            // Assert
            Assert.False(result);
            Assert.Null(semanticVersion);
        }

        [Theory]
        [InlineData("1.2.4.5", new string[] { "1", "2", "4", "5" })]
        [InlineData("01.02.4.5-gamma", new string[] { "01", "02", "4", "5" })]
        [InlineData("1.02.3", new string[] { "1", "02", "3", "0" })]
        [InlineData("1.02.3-beta", new string[] { "1", "02", "3", "0" })]
        [InlineData("00100.02", new string[] { "00100", "02", "0", "0" })]
        [InlineData("00100.02-alpha", new string[] { "00100", "02", "0", "0" })]
        [InlineData("00100.02-alpha.1+2", new string[] { "00100", "02", "0", "0" })]
        public void TestGetOriginalVersionComponents(
            string originalVersion, string[] expectedComponents)
        {
            // Arrange
            var semVer = new SemanticVersion(originalVersion);

            // Act
            string[] components = semVer.GetOriginalVersionComponents();

            // Assert
            Assert.Equal(expectedComponents, components);
        }

        [Theory]
        [InlineData("1.2.4.5", new string[] { "1", "2", "4", "5" })]
        [InlineData("1.02.3", new string[] { "1", "2", "3", "0" })]
        [InlineData("00100.02", new string[] { "100", "2", "0", "0" })]
        public void TestGetOriginalVersionComponentsWhenPassingVersion(
            string originalVersion, string[] expectedComponents)
        {
            // Arrange
            var version = new Version(originalVersion);
            var semVer = new SemanticVersion(version);

            // Act
            string[] components = semVer.GetOriginalVersionComponents();

            // Assert
            Assert.Equal(expectedComponents, components);
        }

        [Theory]
        [InlineData("1.0", "1.0.0")]
        [InlineData("1.7", "1.7.0")]
        [InlineData("1.0.0.0", "1.0.0")]
        [InlineData("1.0.0", "1.0.0")]
        [InlineData("1.2.3", "1.2.3")]
        [InlineData("1.2.03", "1.2.3")]
        [InlineData("1.2.0.4", "1.2.0.4")]
        [InlineData("1.2.3.4", "1.2.3.4")]
        [InlineData("1.2-special", "1.2.0-special")]
        [InlineData("1.2.3-special", "1.2.3-special")]
        [InlineData("1.2.3.5-special", "1.2.3.5-special")]
        [InlineData("1.2.0.5-special", "1.2.0.5-special")]
        [InlineData("1.2.0.5-special.1", "1.2.0.5-special.1")]
        [InlineData("1.2.0.5-special.1+a", "1.2.0.5-special.1")]
        public void NormalizeVersion_NormalizesVersionTo3Digits(string versionString, string expected)
        {
            // Arrange
            var version = new SemanticVersion(versionString);

            // Act
            var result = version.ToNormalizedString();

            // Assert
            Assert.Equal(result, expected);
        }

        [Theory]
        [InlineData("1.0.0")]
        [InlineData("0.0.1")]
        [InlineData("1.2.3")]
        [InlineData("1.2.3-alpha")]
        [InlineData("1.2.3-X.yZ.3.234.243.32423423.4.23423.4324.234.234.3242")]
        [InlineData("1.2.3-X.yZ.3.234.243.32423423.4.23423+METADATA")]
        [InlineData("1.2.3-X.y3+0")]
        [InlineData("1.2.3-X+0")]
        [InlineData("1.2.3+0")]
        [InlineData("1.2.3-0")]
        public void ParseSemanticVersionStrict(string versionString)
        {
            // Act
            SemanticVersion semVer = null;
            SemanticVersion.TryParseStrict(versionString, out semVer);

            // Assert
            Assert.Equal<string>(versionString, semVer.ToFullString());
            Assert.Equal<string>(semVer.ToNormalizedString(), semVer.ToString());
        }

        [Theory]
        [InlineData("1.2.3")]
        [InlineData("1.2.3+0")]
        [InlineData("1.2.3+321")]
        [InlineData("1.2.3+XYZ")]
        public void SemanticVersionStrictEquality(string versionString)
        {
            // Act
            SemanticVersion main = null;
            SemanticVersion.TryParseStrict("1.2.3", out main);

            SemanticVersion semVer = null;
            SemanticVersion.TryParseStrict(versionString, out semVer);

            // Assert
            Assert.True(main.Equals(semVer));
            Assert.True(semVer.Equals(main));

            Assert.True(main.GetHashCode() == semVer.GetHashCode());
        }

        [Theory]
        [InlineData("1.2.3-alpha")]
        [InlineData("1.2.3-alpha+0")]
        [InlineData("1.2.3-alpha+10")]
        [InlineData("1.2.3-alpha+beta")]
        public void SemanticVersionStrictEqualityPreRelease(string versionString)
        {
            // Act
            SemanticVersion main = null;
            SemanticVersion.TryParseStrict("1.2.3-alpha", out main);

            SemanticVersion semVer = null;
            SemanticVersion.TryParseStrict(versionString, out semVer);

            // Assert
            Assert.True(main.Equals(semVer));
            Assert.True(semVer.Equals(main));

            Assert.True(main.GetHashCode() == semVer.GetHashCode());
        }

        [Theory]
        [InlineData("2.7")]
        [InlineData("1.3.4.5")]
        [InlineData("1.3-alpha")]
        [InlineData("1.3 .4")]
        [InlineData("2.3.18.2-a")]
        [InlineData("1.2.3-A..B")]
        [InlineData("01.2.3")]
        [InlineData("1.02.3")]
        [InlineData("1.2.03")]
        [InlineData(".2.03")]
        [InlineData("1.2.")]
        [InlineData("1.2.3-a$b")]
        [InlineData("a.b.c")]
        [InlineData("1.2.3-00")]
        [InlineData("1.2.3-A.00.B")]
        public void TryParseStrictReturnsFalseIfVersionIsNotStrictSemVer200(string version)
        {
            // Act 
            SemanticVersion semanticVersion;
            var result = SemanticVersion.TryParseStrict(version, out semanticVersion);

            // Assert
            Assert.False(result);
            Assert.Null(semanticVersion);
        }

        [Theory]
        [InlineData("1.0.0+alpha", "alpha")]
        [InlineData("1.0.0-1.1+a", "a")]
        public void MetadataTest(string version, string metadata)
        {
            // Act
            SemanticVersion semVer = new SemanticVersion(version);

            // Assert
            Assert.Equal(metadata, semVer.Metadata);
        }
    }
}
