using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Serialization.Objects;
using JsonApiDotNetCoreExample;
using JsonApiDotNetCoreExample.Data;
using JsonApiDotNetCoreExample.Models;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Newtonsoft.Json;
using Xunit;

namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility
{
    [Collection("WebHostCollection")]
    public sealed class IgnoreNullValuesTests : IAsyncLifetime
    {
        private readonly AppDbContext _dbContext;
        private readonly TodoItem _todoItem;

        public IgnoreNullValuesTests(TestFixture<TestStartup> fixture)
        {
            _dbContext = fixture.GetService<AppDbContext>();
            _todoItem = new TodoItem
            {
                Description = null,
                Ordinal = 1,
                CreatedDate = new DateTime(2002, 2,2),
                AchievedDate = new DateTime(2002, 2,4),
                Owner = new Person { FirstName = "Bob", LastName = null }
            };
            _dbContext.TodoItems.Add(_todoItem);
        }

        public async Task InitializeAsync()
        {
            await _dbContext.SaveChangesAsync();
        }

        public Task DisposeAsync()
        {
            return Task.CompletedTask;
        }

        [Theory]
        [InlineData(null, null, null, NullValueHandling.Include)]
        [InlineData(null, null, "false", NullValueHandling.Include)]
        [InlineData(null, null, "true", NullValueHandling.Include)]
        [InlineData(null, null, "unknown", null)]
        [InlineData(null, null, "", null)]
        [InlineData(null, false, null, NullValueHandling.Include)]
        [InlineData(null, false, "false", NullValueHandling.Include)]
        [InlineData(null, false, "true", NullValueHandling.Include)]
        [InlineData(null, false, "unknown", null)]
        [InlineData(null, false, "", null)]
        [InlineData(null, true, null, NullValueHandling.Include)]
        [InlineData(null, true, "false", NullValueHandling.Ignore)]
        [InlineData(null, true, "true", NullValueHandling.Include)]
        [InlineData(null, true, "unknown", null)]
        [InlineData(null, true, "", null)]
        [InlineData(NullValueHandling.Ignore, null, null, NullValueHandling.Ignore)]
        [InlineData(NullValueHandling.Ignore, null, "false", NullValueHandling.Ignore)]
        [InlineData(NullValueHandling.Ignore, null, "true", NullValueHandling.Ignore)]
        [InlineData(NullValueHandling.Ignore, null, "unknown", null)]
        [InlineData(NullValueHandling.Ignore, null, "", null)]
        [InlineData(NullValueHandling.Ignore, false, null, NullValueHandling.Ignore)]
        [InlineData(NullValueHandling.Ignore, false, "false", NullValueHandling.Ignore)]
        [InlineData(NullValueHandling.Ignore, false, "true", NullValueHandling.Ignore)]
        [InlineData(NullValueHandling.Ignore, false, "unknown", null)]
        [InlineData(NullValueHandling.Ignore, false, "", null)]
        [InlineData(NullValueHandling.Ignore, true, null, NullValueHandling.Ignore)]
        [InlineData(NullValueHandling.Ignore, true, "false", NullValueHandling.Ignore)]
        [InlineData(NullValueHandling.Ignore, true, "true", NullValueHandling.Include)]
        [InlineData(NullValueHandling.Ignore, true, "unknown", null)]
        [InlineData(NullValueHandling.Ignore, true, "", null)]
        [InlineData(NullValueHandling.Include, null, null, NullValueHandling.Include)]
        [InlineData(NullValueHandling.Include, null, "false", NullValueHandling.Include)]
        [InlineData(NullValueHandling.Include, null, "true", NullValueHandling.Include)]
        [InlineData(NullValueHandling.Include, null, "unknown", null)]
        [InlineData(NullValueHandling.Include, null, "", null)]
        [InlineData(NullValueHandling.Include, false, null, NullValueHandling.Include)]
        [InlineData(NullValueHandling.Include, false, "false", NullValueHandling.Include)]
        [InlineData(NullValueHandling.Include, false, "true", NullValueHandling.Include)]
        [InlineData(NullValueHandling.Include, false, "unknown", null)]
        [InlineData(NullValueHandling.Include, false, "", null)]
        [InlineData(NullValueHandling.Include, true, null, NullValueHandling.Include)]
        [InlineData(NullValueHandling.Include, true, "false", NullValueHandling.Ignore)]
        [InlineData(NullValueHandling.Include, true, "true", NullValueHandling.Include)]
        [InlineData(NullValueHandling.Include, true, "unknown", null)]
        [InlineData(NullValueHandling.Include, true, "", null)]
        public async Task CheckBehaviorCombination(NullValueHandling? defaultValue, bool? allowQueryStringOverride, string queryStringValue, NullValueHandling? expected)
        {
            var builder = WebHost.CreateDefaultBuilder().UseStartup<TestStartup>();
            var server = new TestServer(builder);
            var services = server.Host.Services;
            var client = server.CreateClient();

            var options = (JsonApiOptions)services.GetService(typeof(IJsonApiOptions));

            if (defaultValue != null)
            {
                options.SerializerSettings.NullValueHandling = defaultValue.Value;
            }
            if (allowQueryStringOverride != null)
            {
                options.AllowQueryStringOverrideForSerializerNullValueHandling = allowQueryStringOverride.Value;
            }

            var queryString = queryStringValue != null
                ? $"&nulls={queryStringValue}"
                : "";
            var route = $"/api/v1/todoItems/{_todoItem.Id}?include=owner{queryString}";
            var request = new HttpRequestMessage(HttpMethod.Get, route);

            // Act
            var response = await client.SendAsync(request);
            var body = await response.Content.ReadAsStringAsync();

            var isQueryStringValueEmpty = queryStringValue == string.Empty;
            var isDisallowedOverride = !options.AllowQueryStringOverrideForSerializerNullValueHandling && queryStringValue != null;
            var isQueryStringInvalid = queryStringValue != null && !bool.TryParse(queryStringValue, out _);

            if (isQueryStringValueEmpty)
            {
                Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

                var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
                Assert.Single(errorDocument.Errors);
                Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode);
                Assert.Equal("Missing query string parameter value.", errorDocument.Errors[0].Title);
                Assert.Equal("Missing value for 'nulls' query string parameter.", errorDocument.Errors[0].Detail);
                Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter);
            }
            else if (isDisallowedOverride)
            {
                Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

                var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
                Assert.Single(errorDocument.Errors);
                Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode);
                Assert.Equal("Usage of one or more query string parameters is not allowed at the requested endpoint.", errorDocument.Errors[0].Title);
                Assert.Equal("The parameter 'nulls' cannot be used at this endpoint.", errorDocument.Errors[0].Detail);
                Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter);
            }
            else if (isQueryStringInvalid)
            {
                Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

                var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
                Assert.Single(errorDocument.Errors);
                Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode);
                Assert.Equal("The specified nulls is invalid.", errorDocument.Errors[0].Title);
                Assert.Equal("The value 'unknown' must be 'true' or 'false'.", errorDocument.Errors[0].Detail);
                Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter);
            }
            else
            {
                if (expected == null)
                {
                    throw new Exception("Invalid test combination. Should never get here.");
                }

                Assert.Equal(HttpStatusCode.OK, response.StatusCode);

                var deserializeBody = JsonConvert.DeserializeObject<Document>(body);
                Assert.Equal(expected == NullValueHandling.Include, deserializeBody.SingleData.Attributes.ContainsKey("description"));
                Assert.Equal(expected == NullValueHandling.Include, deserializeBody.Included[0].Attributes.ContainsKey("lastName"));
            }
        }
    }
}
