// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
    public class FormFileModelBinderTest
    {
        [Fact]
        public async Task FormFileModelBinder_SingleFile_BindSuccessful()
        {
            // Arrange
            var formFiles = new FormFileCollection();
            formFiles.Add(GetMockFormFile("file", "file1.txt"));
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContext(typeof(IEnumerable<IFormFile>), httpContext);
            var binder = new FormFileModelBinder();

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.True(bindingContext.Result.IsModelSet);

            var entry = bindingContext.ValidationState[bindingContext.Result.Model];
            Assert.False(entry.SuppressValidation);
            Assert.Equal("file", entry.Key);
            Assert.Null(entry.Metadata);
        }

        [Fact]
        public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful()
        {
            // Arrange
            var formFiles = GetTwoFiles();
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContext(typeof(IEnumerable<IFormFile>), httpContext);
            var binder = new FormFileModelBinder();

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.True(bindingContext.Result.IsModelSet);

            var entry = bindingContext.ValidationState[bindingContext.Result.Model];
            Assert.False(entry.SuppressValidation);
            Assert.Equal("file", entry.Key);
            Assert.Null(entry.Metadata);

            var files = Assert.IsAssignableFrom<IList<IFormFile>>(bindingContext.Result.Model);
            Assert.Equal(2, files.Count);
        }

        [Theory]
        [InlineData(typeof(IFormFile[]))]
        [InlineData(typeof(ICollection<IFormFile>))]
        [InlineData(typeof(IList<IFormFile>))]
        [InlineData(typeof(IFormFileCollection))]
        [InlineData(typeof(List<IFormFile>))]
        [InlineData(typeof(LinkedList<IFormFile>))]
        [InlineData(typeof(FileList))]
        [InlineData(typeof(FormFileCollection))]
        public async Task FormFileModelBinder_BindsFiles_ForCollectionsItCanCreate(Type destinationType)
        {
            // Arrange
            var binder = new FormFileModelBinder();
            var formFiles = GetTwoFiles();
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContext(destinationType, httpContext);

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.True(bindingContext.Result.IsModelSet);
            Assert.IsAssignableFrom(destinationType, bindingContext.Result.Model);
            Assert.Equal(formFiles, bindingContext.Result.Model as IEnumerable<IFormFile>);
        }

        [Fact]
        public async Task FormFileModelBinder_ExpectSingleFile_BindFirstFile()
        {
            // Arrange
            var formFiles = GetTwoFiles();
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
            var binder = new FormFileModelBinder();

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.True(bindingContext.Result.IsModelSet);
            var file = Assert.IsAssignableFrom<IFormFile>(bindingContext.Result.Model);
            Assert.Equal("file1.txt", file.FileName);
        }

        [Fact]
        public async Task FormFileModelBinder_ReturnsFailedResult_WhenNoFilePosted()
        {
            // Arrange
            var formFiles = new FormFileCollection();
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
            var binder = new FormFileModelBinder();

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.False(bindingContext.Result.IsModelSet);
            Assert.Null(bindingContext.Result.Model);
        }

        [Fact]
        public async Task FormFileModelBinder_ReturnsFailedResult_WhenNamesDoNotMatch()
        {
            // Arrange
            var formFiles = new FormFileCollection();
            formFiles.Add(GetMockFormFile("different name", "file1.txt"));
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
            var binder = new FormFileModelBinder();

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.False(bindingContext.Result.IsModelSet);
            Assert.Null(bindingContext.Result.Model);
        }

        [Theory]
        [InlineData(true, "FieldName")]
        [InlineData(false, "ModelName")]
        public async Task FormFileModelBinder_UsesFieldNameForTopLevelObject(bool isTopLevel, string expected)
        {
            // Arrange
            var formFiles = new FormFileCollection();
            formFiles.Add(GetMockFormFile("FieldName", "file1.txt"));
            formFiles.Add(GetMockFormFile("ModelName", "file1.txt"));
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));

            var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
            bindingContext.IsTopLevelObject = isTopLevel;
            bindingContext.FieldName = "FieldName";
            bindingContext.ModelName = "ModelName";

            var binder = new FormFileModelBinder();

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.True(bindingContext.Result.IsModelSet);
            var file = Assert.IsAssignableFrom<IFormFile>(bindingContext.Result.Model);

            Assert.Equal(expected, file.Name);
        }

        [Fact]
        public async Task FormFileModelBinder_ReturnsFailedResult_WithEmptyContentDisposition()
        {
            // Arrange
            var formFiles = new FormFileCollection();
            formFiles.Add(new Mock<IFormFile>().Object);
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
            var binder = new FormFileModelBinder();

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.False(bindingContext.Result.IsModelSet);
            Assert.Null(bindingContext.Result.Model);
        }

        [Fact]
        public async Task FormFileModelBinder_ReturnsFailedResult_WithNoFileNameAndZeroLength()
        {
            // Arrange
            var formFiles = new FormFileCollection();
            formFiles.Add(GetMockFormFile("file", ""));
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
            var binder = new FormFileModelBinder();

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.False(bindingContext.Result.IsModelSet);
            Assert.Null(bindingContext.Result.Model);
        }

        [Fact]
        public async Task FormFileModelBinder_ReturnsResult_ForReadOnlyDestination()
        {
            // Arrange
            var binder = new FormFileModelBinder();
            var formFiles = GetTwoFiles();
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContextForReadOnlyArray(httpContext);

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.True(bindingContext.Result.IsModelSet);
            Assert.NotNull(bindingContext.Result.Model);
        }

        [Fact]
        public async Task FormFileModelBinder_ReturnsFailedResult_ForCollectionsItCannotCreate()
        {
            // Arrange
            var binder = new FormFileModelBinder();
            var formFiles = GetTwoFiles();
            var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
            var bindingContext = GetBindingContext(typeof(ISet<IFormFile>), httpContext);

            // Act
            await binder.BindModelAsync(bindingContext);

            // Assert
            Assert.False(bindingContext.Result.IsModelSet);
            Assert.Null(bindingContext.Result.Model);
        }

        private static DefaultModelBindingContext GetBindingContextForReadOnlyArray(HttpContext httpContext)
        {
            var metadataProvider = new TestModelMetadataProvider();
            metadataProvider
                .ForProperty<ModelWithReadOnlyArray>(nameof(ModelWithReadOnlyArray.ArrayProperty))
                .BindingDetails(bd => bd.BindingSource = BindingSource.Header);
            var metadata = metadataProvider.GetMetadataForProperty(
                typeof(ModelWithReadOnlyArray),
                nameof(ModelWithReadOnlyArray.ArrayProperty));

            return GetBindingContext(metadata, httpContext);
        }

        private static DefaultModelBindingContext GetBindingContext(Type modelType, HttpContext httpContext)
        {
            var metadataProvider = new EmptyModelMetadataProvider();
            var metadata = metadataProvider.GetMetadataForType(modelType);

            return GetBindingContext(metadata, httpContext);
        }

        private static DefaultModelBindingContext GetBindingContext(
            ModelMetadata metadata,
            HttpContext httpContext)
        {
            var bindingContext = new DefaultModelBindingContext
            {
                ActionContext = new ActionContext()
                {
                    HttpContext = httpContext,
                },
                ModelMetadata = metadata,
                ModelName = "file",
                ModelState = new ModelStateDictionary(),
                ValidationState = new ValidationStateDictionary(),
            };

            return bindingContext;
        }

        private static HttpContext GetMockHttpContext(IFormCollection formCollection)
        {
            var httpContext = new Mock<HttpContext>();
            httpContext.Setup(h => h.Request.ReadFormAsync(It.IsAny<CancellationToken>()))
                .Returns(Task.FromResult(formCollection));
            httpContext.Setup(h => h.Request.HasFormContentType).Returns(true);
            return httpContext.Object;
        }

        private static FormFileCollection GetTwoFiles()
        {
            var formFiles = new FormFileCollection
            {
                GetMockFormFile("file", "file1.txt"),
                GetMockFormFile("file", "file2.txt"),
            };

            return formFiles;
        }

        private static IFormCollection GetMockFormCollection(FormFileCollection formFiles)
        {
            var formCollection = new Mock<IFormCollection>();
            formCollection.Setup(f => f.Files).Returns(formFiles);
            return formCollection.Object;
        }

        private static IFormFile GetMockFormFile(string modelName, string filename)
        {
            var formFile = new Mock<IFormFile>();
            formFile.Setup(f => f.Name).Returns(modelName);
            formFile.Setup(f => f.FileName).Returns(filename);

            return formFile.Object;
        }

        private class ModelWithReadOnlyArray
        {
            public IFormFile[] ArrayProperty { get; }
        }

        private class FileList : List<IFormFile>
        {
        }
    }
}
