// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace Microsoft.AspNetCore.Components.Forms
{
    public class EditContextTest
    {
        [Fact]
        public void CannotUseNullModel()
        {
            var ex = Assert.Throws<ArgumentNullException>(() => new EditContext(null));
            Assert.Equal("model", ex.ParamName);
        }

        [Fact]
        public void CanGetModel()
        {
            var model = new object();
            var editContext = new EditContext(model);
            Assert.Same(model, editContext.Model);
        }

        [Fact]
        public void CanConstructFieldIdentifiersForRootModel()
        {
            // Arrange/Act
            var model = new object();
            var editContext = new EditContext(model);
            var fieldIdentifier = editContext.Field("testFieldName");

            // Assert
            Assert.Same(model, fieldIdentifier.Model);
            Assert.Equal("testFieldName", fieldIdentifier.FieldName);
        }

        [Fact]
        public void IsInitiallyUnmodified()
        {
            var editContext = new EditContext(new object());
            Assert.False(editContext.IsModified());
        }

        [Fact]
        public void TracksFieldsAsModifiedWhenValueChanged()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var fieldOnThisModel1 = editContext.Field("field1");
            var fieldOnThisModel2 = editContext.Field("field2");
            var fieldOnOtherModel = new FieldIdentifier(new object(), "field on other model");

            // Act
            editContext.NotifyFieldChanged(fieldOnThisModel1);
            editContext.NotifyFieldChanged(fieldOnOtherModel);

            // Assert
            Assert.True(editContext.IsModified());
            Assert.True(editContext.IsModified(fieldOnThisModel1));
            Assert.False(editContext.IsModified(fieldOnThisModel2));
            Assert.True(editContext.IsModified(fieldOnOtherModel));
        }

        [Fact]
        public void CanClearIndividualModifications()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var fieldThatWasModified = editContext.Field("field1");
            var fieldThatRemainsModified = editContext.Field("field2");
            var fieldThatWasNeverModified = editContext.Field("field that was never modified");
            editContext.NotifyFieldChanged(fieldThatWasModified);
            editContext.NotifyFieldChanged(fieldThatRemainsModified);

            // Act
            editContext.MarkAsUnmodified(fieldThatWasModified);
            editContext.MarkAsUnmodified(fieldThatWasNeverModified);

            // Assert
            Assert.True(editContext.IsModified());
            Assert.False(editContext.IsModified(fieldThatWasModified));
            Assert.True(editContext.IsModified(fieldThatRemainsModified));
            Assert.False(editContext.IsModified(fieldThatWasNeverModified));
        }

        [Fact]
        public void CanClearAllModifications()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var field1 = editContext.Field("field1");
            var field2 = editContext.Field("field2");
            editContext.NotifyFieldChanged(field1);
            editContext.NotifyFieldChanged(field2);

            // Act
            editContext.MarkAsUnmodified();

            // Assert
            Assert.False(editContext.IsModified());
            Assert.False(editContext.IsModified(field1));
            Assert.False(editContext.IsModified(field2));
        }

        [Fact]
        public void RaisesEventWhenFieldIsChanged()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var field1 = new FieldIdentifier(new object(), "fieldname"); // Shows it can be on a different model
            var didReceiveNotification = false;
            editContext.OnFieldChanged += (sender, eventArgs) =>
            {
                Assert.Same(editContext, sender);
                Assert.Equal(field1, eventArgs.FieldIdentifier);
                didReceiveNotification = true;
            };

            // Act
            editContext.NotifyFieldChanged(field1);

            // Assert
            Assert.True(didReceiveNotification);
        }

        [Fact]
        public void CanEnumerateValidationMessagesAcrossAllStoresForSingleField()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var store1 = new ValidationMessageStore(editContext);
            var store2 = new ValidationMessageStore(editContext);
            var field = new FieldIdentifier(new object(), "field");
            var fieldWithNoState = new FieldIdentifier(new object(), "field with no state");
            store1.Add(field, "Store 1 message 1");
            store1.Add(field, "Store 1 message 2");
            store1.Add(new FieldIdentifier(new object(), "otherfield"), "Message for other field that should not appear in results");
            store2.Add(field, "Store 2 message 1");

            // Act/Assert: Can pick out the messages for a field
            Assert.Equal(new[]
            {
                "Store 1 message 1",
                "Store 1 message 2",
                "Store 2 message 1",
            }, editContext.GetValidationMessages(field).OrderBy(x => x)); // Sort because the order isn't defined

            // Act/Assert: It's fine to ask for messages for a field with no associated state
            Assert.Empty(editContext.GetValidationMessages(fieldWithNoState));

            // Act/Assert: After clearing a single store, we only see the results from other stores
            store1.Clear(field);
            Assert.Equal(new[] { "Store 2 message 1", }, editContext.GetValidationMessages(field));
        }

        [Fact]
        public void CanEnumerateValidationMessagesAcrossAllStoresForAllFields()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var store1 = new ValidationMessageStore(editContext);
            var store2 = new ValidationMessageStore(editContext);
            var field1 = new FieldIdentifier(new object(), "field1");
            var field2 = new FieldIdentifier(new object(), "field2");
            store1.Add(field1, "Store 1 field 1 message 1");
            store1.Add(field1, "Store 1 field 1 message 2");
            store1.Add(field2, "Store 1 field 2 message 1");
            store2.Add(field1, "Store 2 field 1 message 1");

            // Act/Assert
            Assert.Equal(new[]
            {
                "Store 1 field 1 message 1",
                "Store 1 field 1 message 2",
                "Store 1 field 2 message 1",
                "Store 2 field 1 message 1",
            }, editContext.GetValidationMessages().OrderBy(x => x)); // Sort because the order isn't defined

            // Act/Assert: After clearing a single store, we only see the results from other stores
            store1.Clear();
            Assert.Equal(new[] { "Store 2 field 1 message 1", }, editContext.GetValidationMessages());
        }

        [Fact]
        public void IsValidWithNoValidationMessages()
        {
            // Arrange
            var editContext = new EditContext(new object());

            // Act
            var isValid = editContext.Validate();

            // assert
            Assert.True(isValid);
        }

        [Fact]
        public void IsInvalidWithValidationMessages()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var messages = new ValidationMessageStore(editContext);
            messages.Add(
                new FieldIdentifier(new object(), "some field"),
                "Some message");

            // Act
            var isValid = editContext.Validate();

            // assert
            Assert.False(isValid);
        }

        [Fact]
        public void RequestsValidationWhenValidateIsCalled()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var messages = new ValidationMessageStore(editContext);
            editContext.OnValidationRequested += (sender, eventArgs) =>
            {
                Assert.Same(editContext, sender);
                Assert.NotNull(eventArgs);
                messages.Add(
                    new FieldIdentifier(new object(), "some field"),
                    "Some message");
            };

            // Act
            var isValid = editContext.Validate();

            // assert
            Assert.False(isValid);
            Assert.Equal(new[] { "Some message" }, editContext.GetValidationMessages());
        }

        [Fact]
        public void LookingUpModel_ThatOverridesGetHashCodeAndEquals_Works()
        {
            // Test for https://github.com/aspnet/AspNetCore/issues/18069
            // Arrange
            var model = new EquatableModel();
            var editContext = new EditContext(model);

            editContext.NotifyFieldChanged(editContext.Field(nameof(EquatableModel.Property)));

            model.Property = "new value";

            Assert.True(editContext.IsModified(editContext.Field(nameof(EquatableModel.Property))));
        }

        [Fact]
        public void Properties_CanRetrieveViaIndexer()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var key1 = new object();
            var key2 = new object();
            var key3 = new object();
            var value1 = new object();
            var value2 = new object();

            // Initially, the values are not present
            Assert.Throws<KeyNotFoundException>(() => editContext.Properties[key1]);

            // Can store and retrieve values
            editContext.Properties[key1] = value1;
            editContext.Properties[key2] = value2;
            Assert.Same(value1, editContext.Properties[key1]);
            Assert.Same(value2, editContext.Properties[key2]);

            // Unrelated keys are still not found
            Assert.Throws<KeyNotFoundException>(() => editContext.Properties[key3]);
        }

        [Fact]
        public void Properties_CanRetrieveViaTryGetValue()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var key1 = new object();
            var key2 = new object();
            var key3 = new object();
            var value1 = new object();
            var value2 = new object();

            // Initially, the values are not present
            Assert.False(editContext.Properties.TryGetValue(key1, out _));

            // Can store and retrieve values
            editContext.Properties[key1] = value1;
            editContext.Properties[key2] = value2;
            Assert.True(editContext.Properties.TryGetValue(key1, out var retrievedValue1));
            Assert.True(editContext.Properties.TryGetValue(key2, out var retrievedValue2));
            Assert.Same(value1, retrievedValue1);
            Assert.Same(value2, retrievedValue2);

            // Unrelated keys are still not found
            Assert.False(editContext.Properties.TryGetValue(key3, out _));
        }

        [Fact]
        public void Properties_CanRemove()
        {
            // Arrange
            var editContext = new EditContext(new object());
            var key = new object();
            var value = new object();
            editContext.Properties[key] = value;

            // Act
            var resultForExistingKey = editContext.Properties.Remove(key);
            var resultForNonExistingKey = editContext.Properties.Remove(new object());

            // Assert
            Assert.True(resultForExistingKey);
            Assert.False(resultForNonExistingKey);
            Assert.False(editContext.Properties.TryGetValue(key, out _));
            Assert.Throws<KeyNotFoundException>(() => editContext.Properties[key]);
        }

        class EquatableModel : IEquatable<EquatableModel>
        {
            public string Property { get; set; } = "";

            public bool Equals(EquatableModel other)
            {
                return string.Equals(Property, other?.Property, StringComparison.Ordinal);
            }

            public override int GetHashCode()
            {
                return StringComparer.Ordinal.GetHashCode(Property);
            }
        }
    }
}
