// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

namespace System.Data.Entity.Internal
{
    using System.Data.Entity.Core.Objects.DataClasses;
    using System.Data.Entity.Resources;
    using System.Threading;
    using Xunit;

    public class InternalReferenceEntryTests
    {
        [Fact]
        public void MethodInfo_fields_are_initialized()
        {
            Assert.NotNull(InternalReferenceEntry.SetValueOnEntityReferenceMethod);
        }

        public class CurrentValue
        {
            [Fact]
            public void InternalReferenceEntry_gets_current_value_from_entity_if_property_exists()
            {
                InternalReferenceEntry_gets_current_value_from_entity_if_property_exists_implementation(isDetached: false);
            }

            [Fact]
            public void InternalReferenceEntry_gets_current_value_from_entity_even_when_detached_if_property_exists()
            {
                InternalReferenceEntry_gets_current_value_from_entity_if_property_exists_implementation(isDetached: true);
            }

            private void InternalReferenceEntry_gets_current_value_from_entity_if_property_exists_implementation(bool isDetached)
            {
                var relatedEntity = new FakeEntity();
                var entity = new FakeWithProps
                                 {
                                     Reference = relatedEntity
                                 };
                var internalEntry = new InternalReferenceEntry(
                    MockHelper.CreateMockInternalEntityEntry(
                        entity, isDetached).Object, FakeWithProps.ReferenceMetadata);

                var propValue = internalEntry.CurrentValue;

                Assert.Same(relatedEntity, propValue);
            }

            [Fact]
            public void InternalReferenceEntry_sets_current_value_onto_entity_if_property_exists()
            {
                InternalReferenceEntry_sets_current_value_onto_entity_if_property_exists_implementation(isDetached: false);
            }

            [Fact]
            public void InternalReferenceEntry_sets_current_value_onto_entity_even_when_detached_if_property_exists()
            {
                InternalReferenceEntry_sets_current_value_onto_entity_if_property_exists_implementation(isDetached: true);
            }

            private void InternalReferenceEntry_sets_current_value_onto_entity_if_property_exists_implementation(bool isDetached)
            {
                var entity = new FakeWithProps
                                 {
                                     Reference = new FakeEntity()
                                 };
                var internalEntry = new InternalReferenceEntry(
                    MockHelper.CreateMockInternalEntityEntry(entity, isDetached).Object, FakeWithProps.ReferenceMetadata);

                var relatedEntity = new FakeEntity();
                internalEntry.CurrentValue = relatedEntity;

                Assert.Same(relatedEntity, entity.Reference);
            }

            [Fact]
            public void InternalReferenceEntry_gets_current_value_from_RelatedEnd_if_navigation_property_has_been_removed_from_entity()
            {
                InternalReferenceEntry_gets_current_value_from_RelatedEnd_if_navigation_property_has_been_removed_from_entity_implementation
                    (new FakeEntity());
            }

            [Fact]
            public void
                InternalReferenceEntry_gets_null_from_RelatedEnd_if_navigation_property_has_been_removed_from_entity_and_no_value_is_set()
            {
                InternalReferenceEntry_gets_current_value_from_RelatedEnd_if_navigation_property_has_been_removed_from_entity_implementation
                    (null);
            }

            private void
                InternalReferenceEntry_gets_current_value_from_RelatedEnd_if_navigation_property_has_been_removed_from_entity_implementation
                (FakeEntity relatedEntity)
            {
                var mockRelatedEnd = Core.Objects.DataClasses.MockHelper.CreateMockEntityReference(relatedEntity);

                var internalEntry =
                    new InternalReferenceEntry(
                        MockHelper.CreateMockInternalEntityEntry(new FakeEntity(), mockRelatedEnd.Object).Object,
                        FakeWithProps.ReferenceMetadata);

                var propValue = internalEntry.CurrentValue;

                Assert.Same(relatedEntity, propValue);
            }

            [Fact]
            public void InternalReferenceEntry_sets_current_value_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity()
            {
                InternalReferenceEntry_sets_current_value_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity_implementation
                    (new FakeEntity(), new FakeEntity());
            }

            [Fact]
            public void InternalReferenceEntry_sets_null_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity()
            {
                InternalReferenceEntry_sets_current_value_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity_implementation
                    (new FakeEntity(), null);
            }

            [Fact]
            public void
                InternalReferenceEntry_sets_current_value_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity_when_current_value_is_null
                ()
            {
                InternalReferenceEntry_sets_current_value_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity_implementation
                    (null, new FakeEntity());
            }

            [Fact]
            public void
                InternalReferenceEntry_sets_null_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity_when_current_value_and_new_value_are_both_null
                ()
            {
                InternalReferenceEntry_sets_current_value_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity_implementation
                    (null, null);
            }

            [Fact]
            public void
                InternalReferenceEntry_does_not_set_anything_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity_when_current_value_and_new_value_are_same
                ()
            {
                var relatedEntity = new FakeEntity();
                InternalReferenceEntry_sets_current_value_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity_implementation
                    (relatedEntity, relatedEntity);
            }

            /// <summary>
            /// Validates that the call to set a value on an <see cref="EntityReference{TEntity}" /> is made as
            /// expected without mocking EntityReference (since it is sealed). This could be dones with Moq
            /// but it turned out easier and clearer this way.
            /// </summary>
            internal class FakeInternalReferenceEntry : InternalReferenceEntry
            {
                public FakeInternalReferenceEntry(InternalEntityEntry internalEntityEntry, NavigationEntryMetadata navigationMetadata)
                    : base(internalEntityEntry, navigationMetadata)
                {
                }

                protected override void SetNavigationPropertyOnRelatedEnd(object value)
                {
                    SetCount++;
                    ValueSet = value;
                }

                public object ValueSet { get; set; }
                public int SetCount { get; set; }
            }

            private void
                InternalReferenceEntry_sets_current_value_onto_RelatedEnd_if_navigation_property_has_been_removed_from_entity_implementation
                (FakeEntity currentRelatedEntity, FakeEntity newRelatedEntity)
            {
                var mockRelatedEnd = Core.Objects.DataClasses.MockHelper.CreateMockEntityReference(currentRelatedEntity);

                var internalEntry =
                    new FakeInternalReferenceEntry(
                        MockHelper.CreateMockInternalEntityEntry(new FakeEntity(), mockRelatedEnd.Object).Object,
                        FakeWithProps.ReferenceMetadata);

                internalEntry.CurrentValue = newRelatedEntity;

                Assert.Equal(1, internalEntry.SetCount);
                Assert.Same(newRelatedEntity, internalEntry.ValueSet);
            }

            [Fact]
            public void InternalReferenceEntry_throws_getting_current_value_from_detached_entity_if_navigation_property_has_been_removed()
            {
                var mockInternalEntry = MockHelper.CreateMockInternalEntityEntry(
                    new FakeEntity(), isDetached: true);
                var internalEntry = new InternalReferenceEntry(mockInternalEntry.Object, FakeWithProps.ReferenceMetadata);

                Assert.Equal(
                    Strings.DbPropertyEntry_NotSupportedForDetached("CurrentValue", "Reference", "FakeEntity"),
                    Assert.Throws<InvalidOperationException>(() => { var _ = internalEntry.CurrentValue; }).Message);
            }

            [Fact]
            public void InternalReferenceEntry_throws_setting_current_value_from_detached_entity_if_navigation_property_has_been_removed()
            {
                var mockInternalEntry = MockHelper.CreateMockInternalEntityEntry(
                    new FakeEntity(), isDetached: true);
                var internalEntry = new InternalReferenceEntry(mockInternalEntry.Object, FakeWithProps.ReferenceMetadata);

                Assert.Equal(
                    Strings.DbPropertyEntry_SettingEntityRefNotSupported("Reference", "FakeEntity", "Detached"),
                    Assert.Throws<NotSupportedException>(() => { internalEntry.CurrentValue = null; }).Message);
            }
        }

        public class Load
        {
            [Fact]
            public void InternalReferenceEntry_Load_throws_if_used_with_Detached_entity()
            {
                var mockInternalEntry = MockHelper.CreateMockInternalEntityEntry(
                    new FakeEntity(), isDetached: true);
                var internalEntry = new InternalReferenceEntry(mockInternalEntry.Object, FakeWithProps.ReferenceMetadata);

                Assert.Equal(
                    Strings.DbPropertyEntry_NotSupportedForDetached("Load", "Reference", "FakeEntity"),
                    Assert.Throws<InvalidOperationException>(() => internalEntry.Load()).Message);
            }
        }

#if !NET40

        public class LoadAsync
        {
            [Fact]
            public void InternalReferenceEntry_LoadAsync_throws_if_used_with_Detached_entity()
            {
                var mockInternalEntry = MockHelper.CreateMockInternalEntityEntry(
                    new FakeEntity(), isDetached: true);
                var internalEntry = new InternalReferenceEntry(mockInternalEntry.Object, FakeWithProps.ReferenceMetadata);

                Assert.Equal(
                    Strings.DbPropertyEntry_NotSupportedForDetached("LoadAsync", "Reference", "FakeEntity"),
                    Assert.Throws<InvalidOperationException>(
                        () =>
                        internalEntry.LoadAsync(CancellationToken.None).Wait()).Message);
            }
        }

#endif

        public class IsLoaded
        {
            [Fact]
            public void InternalReferenceEntry_IsLoaded_throws_if_used_with_Detached_entity()
            {
                var mockInternalEntry = MockHelper.CreateMockInternalEntityEntry(
                    new FakeEntity(), isDetached: true);
                var internalEntry = new InternalReferenceEntry(mockInternalEntry.Object, FakeWithProps.ReferenceMetadata);

                Assert.Equal(
                    Strings.DbPropertyEntry_NotSupportedForDetached("IsLoaded", "Reference", "FakeEntity"),
                    Assert.Throws<InvalidOperationException>(() => { var _ = internalEntry.IsLoaded; }).Message);

                Assert.Equal(
                    Strings.DbPropertyEntry_NotSupportedForDetached("IsLoaded", "Reference", "FakeEntity"),
                    Assert.Throws<InvalidOperationException>(() => { internalEntry.IsLoaded = true; }).Message);
            }
        }

        public class Name
        {
            [Fact]
            public void InternalReferenceEntry_Name_works_even_when_used_with_Detached_entity()
            {
                var mockInternalEntry = MockHelper.CreateMockInternalEntityEntry(
                    new FakeEntity(), isDetached: true);
                var internalEntry = new InternalReferenceEntry(mockInternalEntry.Object, FakeWithProps.ReferenceMetadata);

                Assert.Equal("Reference", internalEntry.Name);
            }
        }

        public class Query
        {
            [Fact]
            public void InternalReferenceEntry_Query_throws_if_used_with_Detached_entity()
            {
                var mockInternalEntry = MockHelper.CreateMockInternalEntityEntry(
                    new FakeEntity(), isDetached: true);
                var internalEntry = new InternalReferenceEntry(mockInternalEntry.Object, FakeWithProps.ReferenceMetadata);

                Assert.Equal(
                    Strings.DbPropertyEntry_NotSupportedForDetached("Query", "Reference", "FakeEntity"),
                    Assert.Throws<InvalidOperationException>(() => internalEntry.Query()).Message);
            }
        }
    }
}
