// 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.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Parsing
{
    public class CSharpParseOptionsTests : CSharpTestBase
    {
        private void TestProperty<T>(Func<CSharpParseOptions, T, CSharpParseOptions> factory, Func<CSharpParseOptions, T> getter, T validValue)
        {
            var oldOpt1 = CSharpParseOptions.Default;
            var newOpt1 = factory(oldOpt1, validValue);
            var newOpt2 = factory(newOpt1, validValue);

            Assert.Equal(validValue, getter(newOpt1));
            Assert.Same(newOpt2, newOpt1);
        }

        [Fact]
        [WorkItem(15358, "https://github.com/dotnet/roslyn/issues/15358")]
        public void WithDocumentationModeDoesntChangeFeatures()
        {
            var kvp = new KeyValuePair<string, string>("IOperation", "true");
            var po = new CSharpParseOptions().WithFeatures(new[] { kvp });
            Assert.Equal(po.Features.AsSingleton(), kvp);
            var po2 = po.WithDocumentationMode(DocumentationMode.Diagnose);
            Assert.Equal(po2.Features.AsSingleton(), kvp);
        }

        [Fact]
        public void WithXxx()
        {
            TestProperty((old, value) => old.WithKind(value), opt => opt.Kind, SourceCodeKind.Script);
            TestProperty((old, value) => old.WithLanguageVersion(value), opt => opt.LanguageVersion, LanguageVersion.CSharp3);
            TestProperty((old, value) => old.WithDocumentationMode(value), opt => opt.DocumentationMode, DocumentationMode.None);
            TestProperty((old, value) => old.WithPreprocessorSymbols(value), opt => opt.PreprocessorSymbols, ImmutableArray.Create<string>("A", "B", "C"));
        }

        [Fact]
        public void WithPreprocessorSymbols()
        {
            Assert.Equal(0, CSharpParseOptions.Default.WithPreprocessorSymbols(ImmutableArray.Create("A", "B")).WithPreprocessorSymbols(default(ImmutableArray<string>)).PreprocessorSymbols.Length);
            Assert.Equal(0, CSharpParseOptions.Default.WithPreprocessorSymbols(ImmutableArray.Create("A", "B")).WithPreprocessorSymbols((IEnumerable<string>)null).PreprocessorSymbols.Length);
            Assert.Equal(0, CSharpParseOptions.Default.WithPreprocessorSymbols(ImmutableArray.Create("A", "B")).WithPreprocessorSymbols((string[])null).PreprocessorSymbols.Length);
        }

        [Fact]
        public void WithFeatures()
        {
            var options1 = CSharpParseOptions.Default.WithFeatures(new Dictionary<string, string>() { { "F1", "V1" }, { "F2", "V2" } });
            var options2 = CSharpParseOptions.Default.WithFeatures(new Dictionary<string, string>() { { "f2", "V2" }, { "F1", "V1" } });

            Assert.True(options1.Equals(options2));
        }

        /// <summary>
        /// If this test fails, please update the <see cref="CSharpParseOptions.GetHashCode"/>
        /// and <see cref="CSharpParseOptions.Equals(CSharpParseOptions)"/> methods to
        /// make sure they are doing the right thing with your new field and then update the baseline
        /// here.
        /// </summary>
        [Fact]
        public void TestFieldsForEqualsAndGetHashCode()
        {
            ReflectionAssert.AssertPublicAndInternalFieldsAndProperties(
                typeof(CSharpParseOptions),
                "Features",
                "FileBasedProgram",
                "Language",
                "LanguageVersion",
                "InterceptorsNamespaces",
                "PreprocessorSymbolNames",
                "PreprocessorSymbols",
                "SpecifiedLanguageVersion");
        }

        [Fact]
        public void SpecifiedKindIsMappedCorrectly()
        {
            var options = new CSharpParseOptions();
            Assert.Equal(SourceCodeKind.Regular, options.Kind);
            Assert.Equal(SourceCodeKind.Regular, options.SpecifiedKind);

            options.Errors.Verify();

            options = new CSharpParseOptions(kind: SourceCodeKind.Regular);
            Assert.Equal(SourceCodeKind.Regular, options.Kind);
            Assert.Equal(SourceCodeKind.Regular, options.SpecifiedKind);

            options.Errors.Verify();

            options = new CSharpParseOptions(kind: SourceCodeKind.Script);
            Assert.Equal(SourceCodeKind.Script, options.Kind);
            Assert.Equal(SourceCodeKind.Script, options.SpecifiedKind);

            options.Errors.Verify();

#pragma warning disable CS0618 // SourceCodeKind.Interactive is obsolete
            options = new CSharpParseOptions(kind: SourceCodeKind.Interactive);
            Assert.Equal(SourceCodeKind.Script, options.Kind);
            Assert.Equal(SourceCodeKind.Interactive, options.SpecifiedKind);
#pragma warning restore CS0618 // SourceCodeKind.Interactive is obsolete

            options.Errors.Verify(
                // error CS8190: Provided source code kind is unsupported or invalid: 'Interactive'.
                Diagnostic(ErrorCode.ERR_BadSourceCodeKind).WithArguments("Interactive").WithLocation(1, 1));

            options = new CSharpParseOptions(kind: (SourceCodeKind)int.MinValue);
            Assert.Equal(SourceCodeKind.Regular, options.Kind);
            Assert.Equal((SourceCodeKind)int.MinValue, options.SpecifiedKind);

            options.Errors.Verify(
                // warning CS8190: Provided source code kind is unsupported or invalid: '-2147483648'
                Diagnostic(ErrorCode.ERR_BadSourceCodeKind).WithArguments("-2147483648").WithLocation(1, 1));
        }

        [Fact]
        public void TwoOptionsWithDifferentSpecifiedKindShouldNotHaveTheSameHashCodes()
        {
            var options1 = new CSharpParseOptions(kind: SourceCodeKind.Script);
            var options2 = new CSharpParseOptions(kind: SourceCodeKind.Script);

            Assert.Equal(options1.GetHashCode(), options2.GetHashCode());

            // They both map internally to SourceCodeKind.Script
#pragma warning disable CS0618 // SourceCodeKind.Interactive is obsolete
            options1 = new CSharpParseOptions(kind: SourceCodeKind.Script);
            options2 = new CSharpParseOptions(kind: SourceCodeKind.Interactive);
#pragma warning restore CS0618 // SourceCodeKind.Interactive is obsolete

            Assert.NotEqual(options1.GetHashCode(), options2.GetHashCode());
        }

        [Fact]
        public void TwoOptionsWithDifferentSpecifiedKindShouldNotBeEqual()
        {
            var options1 = new CSharpParseOptions(kind: SourceCodeKind.Script);
            var options2 = new CSharpParseOptions(kind: SourceCodeKind.Script);

            Assert.True(options1.Equals(options2));

            // They both map internally to SourceCodeKind.Script
#pragma warning disable CS0618 // SourceCodeKind.Interactive is obsolete
            options1 = new CSharpParseOptions(kind: SourceCodeKind.Script);
            options2 = new CSharpParseOptions(kind: SourceCodeKind.Interactive);
#pragma warning restore CS0618 // SourceCodeKind.Interactive is obsolete

            Assert.False(options1.Equals(options2));
        }

        [Fact]
        public void BadSourceCodeKindShouldProduceDiagnostics()
        {
#pragma warning disable CS0618 // Type or member is obsolete
            var options = new CSharpParseOptions(kind: SourceCodeKind.Interactive);
#pragma warning restore CS0618 // Type or member is obsolete

            options.Errors.Verify(
                // error CS8190: Provided source code kind is unsupported or invalid: 'Interactive'.
                Diagnostic(ErrorCode.ERR_BadSourceCodeKind).WithArguments("Interactive").WithLocation(1, 1));
        }

        [Fact]
        public void BadDocumentationModeShouldProduceDiagnostics()
        {
            var options = new CSharpParseOptions(documentationMode: unchecked((DocumentationMode)100));

            options.Errors.Verify(
                // error CS8191: Provided documentation mode is unsupported or invalid: '100'.
                Diagnostic(ErrorCode.ERR_BadDocumentationMode).WithArguments("100").WithLocation(1, 1));
        }

        [Fact]
        public void BadLanguageVersionShouldProduceDiagnostics()
        {
            var options = new CSharpParseOptions(languageVersion: unchecked((LanguageVersion)10000));

            options.Errors.Verify(
                // error CS8191: Provided language version is unsupported or invalid: '10000'.
                Diagnostic(ErrorCode.ERR_BadLanguageVersion).WithArguments("10000").WithLocation(1, 1));
        }

        [Fact]
        public void BadPreProcessorSymbolsShouldProduceDiagnostics()
        {
            var options = new CSharpParseOptions(preprocessorSymbols: new[] { "test", "1" });

            options.Errors.Verify(
                // error CS8301: Invalid name for a preprocessing symbol; '1' is not a valid identifier
                Diagnostic(ErrorCode.ERR_InvalidPreprocessingSymbol).WithArguments("1").WithLocation(1, 1));
        }

        [Fact]
        public void BadSourceCodeKindShouldProduceDiagnostics_WithVariation()
        {
#pragma warning disable CS0618 // Type or member is obsolete
            var options = new CSharpParseOptions().WithKind(SourceCodeKind.Interactive);
#pragma warning restore CS0618 // Type or member is obsolete

            options.Errors.Verify(
                // error CS8190: Provided source code kind is unsupported or invalid: 'Interactive'.
                Diagnostic(ErrorCode.ERR_BadSourceCodeKind).WithArguments("Interactive").WithLocation(1, 1));
        }

        [Fact]
        public void BadDocumentationModeShouldProduceDiagnostics_WithVariation()
        {
            var options = new CSharpParseOptions().WithDocumentationMode(unchecked((DocumentationMode)100));

            options.Errors.Verify(
                // error CS8191: Provided documentation mode is unsupported or invalid: '100'.
                Diagnostic(ErrorCode.ERR_BadDocumentationMode).WithArguments("100").WithLocation(1, 1));
        }

        [Fact]
        public void BadLanguageVersionShouldProduceDiagnostics_WithVariation()
        {
            var options = new CSharpParseOptions().WithLanguageVersion(unchecked((LanguageVersion)10000));

            options.Errors.Verify(
                // error CS8191: Provided language version is unsupported or invalid: '10000'.
                Diagnostic(ErrorCode.ERR_BadLanguageVersion).WithArguments("10000").WithLocation(1, 1));
        }

        [Fact]
        public void BadPreProcessorSymbolsShouldProduceDiagnostics_EmptyString()
        {
            var options = new CSharpParseOptions().WithPreprocessorSymbols(new[] { "" });

            options.Errors.Verify(
                // error CS8301: Invalid name for a preprocessing symbol; '' is not a valid identifier
                Diagnostic(ErrorCode.ERR_InvalidPreprocessingSymbol).WithArguments("").WithLocation(1, 1));
        }

        [Fact]
        public void BadPreProcessorSymbolsShouldProduceDiagnostics_WhiteSpaceString()
        {
            var options = new CSharpParseOptions().WithPreprocessorSymbols(new[] { " " });

            options.Errors.Verify(
                // error CS8301: Invalid name for a preprocessing symbol; ' ' is not a valid identifier
                Diagnostic(ErrorCode.ERR_InvalidPreprocessingSymbol).WithArguments(" ").WithLocation(1, 1));
        }

        [Fact]
        public void BadPreProcessorSymbolsShouldProduceDiagnostics_SymbolWithDots()
        {
            var options = new CSharpParseOptions().WithPreprocessorSymbols(new[] { "Good", "Bad.Symbol" });

            options.Errors.Verify(
                // error CS8301: Invalid name for a preprocessing symbol; 'Bad.Symbol' is not a valid identifier
                Diagnostic(ErrorCode.ERR_InvalidPreprocessingSymbol).WithArguments("Bad.Symbol").WithLocation(1, 1));
        }

        [Fact]
        public void BadPreProcessorSymbolsShouldProduceDiagnostics_SymbolWithSlashes()
        {
            var options = new CSharpParseOptions().WithPreprocessorSymbols(new[] { "Good", "Bad\\Symbol" });

            options.Errors.Verify(
                // error CS8301: Invalid name for a preprocessing symbol; 'Bad\Symbol' is not a valid identifier
                Diagnostic(ErrorCode.ERR_InvalidPreprocessingSymbol).WithArguments("Bad\\Symbol").WithLocation(1, 1));
        }

        [Fact]
        public void BadPreProcessorSymbolsShouldProduceDiagnostics_NullSymbol()
        {
            var options = new CSharpParseOptions().WithPreprocessorSymbols(new[] { "Good", null });

            options.Errors.Verify(
                // error CS8301: Invalid name for a preprocessing symbol; 'null' is not a valid identifier
                Diagnostic(ErrorCode.ERR_InvalidPreprocessingSymbol).WithArguments("null").WithLocation(1, 1));
        }
    }
}
