using System;
using System.Collections.Generic;
using System.Data;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Events;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Changes;
using Umbraco.Core.Strings;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing.Objects;
using Umbraco.Tests.Testing.Objects.Accessors;
using Umbraco.Web;
using Umbraco.Web.Cache;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.PublishedCache.NuCache;
using Umbraco.Web.PublishedCache.NuCache.DataSource;

namespace Umbraco.Tests.PublishedContent
{
    [TestFixture]
    public class NuCacheTests
    {
        private IPublishedSnapshotService _snapshotService;
        private IVariationContextAccessor _variationAccesor;
        private ContentType _contentType;
        private PropertyType _propertyType;

        private void Init()
        {
            Current.Reset();

            var factory = Mock.Of<IFactory>();
            Current.Factory = factory;

            var configs = new Configs();
            Mock.Get(factory).Setup(x => x.GetInstance(typeof(Configs))).Returns(configs);
            var globalSettings = new GlobalSettings();
            configs.Add(SettingsForTests.GenerateMockUmbracoSettings);
            configs.Add<IGlobalSettings>(() => globalSettings);

            var publishedModelFactory = new NoopPublishedModelFactory();
            Mock.Get(factory).Setup(x => x.GetInstance(typeof(IPublishedModelFactory))).Returns(publishedModelFactory);

            // create a content node kit
            var kit = new ContentNodeKit
            {
                ContentTypeId = 2,
                Node = new ContentNode(1, Guid.NewGuid(), 0, "-1,1", 0, -1, DateTime.Now, 0),
                DraftData = new ContentData
                {
                    Name = "It Works2!",
                    Published = false,
                    TemplateId = 0,
                    VersionId = 2,
                    VersionDate = DateTime.Now,
                    WriterId = 0,
                    Properties = new Dictionary<string, PropertyData[]> { { "prop", new[]
                    {
                        new PropertyData { Culture = "", Segment = "", Value = "val2" },
                        new PropertyData { Culture = "fr-FR", Segment = "", Value = "val-fr2" },
                        new PropertyData { Culture = "en-UK", Segment = "", Value = "val-uk2" },
                        new PropertyData { Culture = "dk-DA", Segment = "", Value = "val-da2" },
                        new PropertyData { Culture = "de-DE", Segment = "", Value = "val-de2" }
                    } } },
                    CultureInfos = new Dictionary<string, CultureVariation>
                    {
                        // draft data = everything, and IsDraft indicates what's edited
                        { "fr-FR", new CultureVariation { Name = "name-fr2", IsDraft = true, Date = new DateTime(2018, 01, 03, 01, 00, 00) } },
                        { "en-UK", new CultureVariation { Name = "name-uk2", IsDraft = true, Date = new DateTime(2018, 01, 04, 01, 00, 00) } },
                        { "dk-DA", new CultureVariation { Name = "name-da2", IsDraft = true, Date = new DateTime(2018, 01, 05, 01, 00, 00) } },
                        { "de-DE", new CultureVariation { Name = "name-de1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) } }
                    }
                },
                PublishedData = new ContentData
                {
                    Name = "It Works1!",
                    Published = true,
                    TemplateId = 0,
                    VersionId = 1,
                    VersionDate = DateTime.Now,
                    WriterId = 0,
                    Properties = new Dictionary<string, PropertyData[]> { { "prop", new[]
                    {
                        new PropertyData { Culture = "", Segment = "", Value = "val1" },
                        new PropertyData { Culture = "fr-FR", Segment = "", Value = "val-fr1" },
                        new PropertyData { Culture = "en-UK", Segment = "", Value = "val-uk1" }
                    } } },
                    CultureInfos = new Dictionary<string, CultureVariation>
                    {
                        // published data = only what's actually published, and IsDraft has to be false
                        { "fr-FR", new CultureVariation { Name = "name-fr1", IsDraft = false, Date = new DateTime(2018, 01, 01, 01, 00, 00) } },
                        { "en-UK", new CultureVariation { Name = "name-uk1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) } },
                        { "de-DE", new CultureVariation { Name = "name-de1", IsDraft = false, Date = new DateTime(2018, 01, 02, 01, 00, 00) } }
                    }
                }
            };

            // create a data source for NuCache
            var dataSource = new TestDataSource(kit);

            var runtime = Mock.Of<IRuntimeState>();
            Mock.Get(runtime).Setup(x => x.Level).Returns(RuntimeLevel.Run);

            // create data types, property types and content types
            var dataType = new DataType(new VoidEditor("Editor", Mock.Of<ILogger>())) { Id = 3 };

            var dataTypes = new[]
            {
                dataType
            };

            _propertyType = new PropertyType("Umbraco.Void.Editor", ValueStorageType.Nvarchar) { Alias = "prop", DataTypeId = 3, Variations = ContentVariation.Culture };
            _contentType = new ContentType(-1) { Id = 2, Alias = "alias-ct", Variations = ContentVariation.Culture };
            _contentType.AddPropertyType(_propertyType);

            var contentTypes = new[]
            {
                _contentType
            };

            var contentTypeService = Mock.Of<IContentTypeService>();
            Mock.Get(contentTypeService).Setup(x => x.GetAll()).Returns(contentTypes);
            Mock.Get(contentTypeService).Setup(x => x.GetAll(It.IsAny<int[]>())).Returns(contentTypes);

            var contentTypeServiceBaseFactory = Mock.Of<IContentTypeBaseServiceProvider>();
            Mock.Get(contentTypeServiceBaseFactory).Setup(x => x.For(It.IsAny<IContentBase>())).Returns(contentTypeService);

            var dataTypeService = Mock.Of<IDataTypeService>();
            Mock.Get(dataTypeService).Setup(x => x.GetAll()).Returns(dataTypes);

            // create a service context
            var serviceContext = ServiceContext.CreatePartial(
                dataTypeService: dataTypeService,
                memberTypeService: Mock.Of<IMemberTypeService>(),
                memberService: Mock.Of<IMemberService>(),
                contentTypeService: contentTypeService,
                localizationService: Mock.Of<ILocalizationService>()
            );

            // create a scope provider
            var scopeProvider = Mock.Of<IScopeProvider>();
            Mock.Get(scopeProvider)
                .Setup(x => x.CreateScope(
                    It.IsAny<IsolationLevel>(),
                    It.IsAny<RepositoryCacheMode>(),
                    It.IsAny<IEventDispatcher>(),
                    It.IsAny<bool?>(),
                    It.IsAny<bool>(),
                    It.IsAny<bool>()))
                .Returns(Mock.Of<IScope>);

            // create a published content type factory
            var contentTypeFactory = new PublishedContentTypeFactory(
                Mock.Of<IPublishedModelFactory>(),
                new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()),
                dataTypeService);

            // create a variation accessor
            _variationAccesor = new TestVariationContextAccessor();

            // at last, create the complete NuCache snapshot service!
            var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true };
            _snapshotService = new PublishedSnapshotService(options,
                null,
                runtime,
                serviceContext,
                contentTypeFactory,
                null,
                new TestPublishedSnapshotAccessor(),
                _variationAccesor,
                Mock.Of<ILogger>(),
                scopeProvider,
                Mock.Of<IDocumentRepository>(),
                Mock.Of<IMediaRepository>(),
                Mock.Of<IMemberRepository>(),
                new TestDefaultCultureAccessor(),
                dataSource,
                globalSettings,
                Mock.Of<IEntityXmlSerializer>(),
                Mock.Of<IPublishedModelFactory>(),
                new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }));

            // invariant is the current default
            _variationAccesor.VariationContext = new VariationContext();

            Mock.Get(factory).Setup(x => x.GetInstance(typeof(IVariationContextAccessor))).Returns(_variationAccesor);
        }

        [Test]
        public void StandaloneVariations()
        {
            // this test implements a full standalone NuCache (based upon a test IDataSource, does not
            // use any local db files, does not rely on any database) - and tests variations

            Init();

            // get a snapshot, get a published content
            var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null);
            var publishedContent = snapshot.Content.GetById(1);

            Assert.IsNotNull(publishedContent);
            Assert.AreEqual("val1", publishedContent.Value<string>("prop"));
            Assert.AreEqual("val-fr1", publishedContent.Value<string>("prop", "fr-FR"));
            Assert.AreEqual("val-uk1", publishedContent.Value<string>("prop", "en-UK"));

            Assert.IsNull(publishedContent.Name()); // no invariant name for varying content
            Assert.AreEqual("name-fr1", publishedContent.Name("fr-FR"));
            Assert.AreEqual("name-uk1", publishedContent.Name("en-UK"));

            var draftContent = snapshot.Content.GetById(true, 1);
            Assert.AreEqual("val2", draftContent.Value<string>("prop"));
            Assert.AreEqual("val-fr2", draftContent.Value<string>("prop", "fr-FR"));
            Assert.AreEqual("val-uk2", draftContent.Value<string>("prop", "en-UK"));

            Assert.IsNull(draftContent.Name()); // no invariant name for varying content
            Assert.AreEqual("name-fr2", draftContent.Name("fr-FR"));
            Assert.AreEqual("name-uk2", draftContent.Name("en-UK"));

            // now french is default
            _variationAccesor.VariationContext = new VariationContext("fr-FR");
            Assert.AreEqual("val-fr1", publishedContent.Value<string>("prop"));
            Assert.AreEqual("name-fr1", publishedContent.Name());
            Assert.AreEqual(new DateTime(2018, 01, 01, 01, 00, 00), publishedContent.CultureDate());

            // now uk is default
            _variationAccesor.VariationContext = new VariationContext("en-UK");
            Assert.AreEqual("val-uk1", publishedContent.Value<string>("prop"));
            Assert.AreEqual("name-uk1", publishedContent.Name());
            Assert.AreEqual(new DateTime(2018, 01, 02, 01, 00, 00), publishedContent.CultureDate());

            // invariant needs to be retrieved explicitly, when it's not default
            Assert.AreEqual("val1", publishedContent.Value<string>("prop", culture: ""));

            // but,
            // if the content type / property type does not vary, then it's all invariant again
            // modify the content type and property type, notify the snapshot service
            _contentType.Variations = ContentVariation.Nothing;
            _propertyType.Variations = ContentVariation.Nothing;
            _snapshotService.Notify(new[] { new ContentTypeCacheRefresher.JsonPayload("IContentType", publishedContent.ContentType.Id, ContentTypeChangeTypes.RefreshMain) });

            // get a new snapshot (nothing changed in the old one), get the published content again
            var anotherSnapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null);
            var againContent = anotherSnapshot.Content.GetById(1);

            Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.Variations);
            Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.GetPropertyType("prop").Variations);

            // now, "no culture" means "invariant"
            Assert.AreEqual("It Works1!", againContent.Name());
            Assert.AreEqual("val1", againContent.Value<string>("prop"));
        }

        [Test]
        public void IsDraftIsPublished()
        {
            Init();

            // get the published published content
            var s = _snapshotService.CreatePublishedSnapshot(null);
            var c1 = s.Content.GetById(1);

            // published content = nothing is draft here
            Assert.IsFalse(c1.IsDraft("fr-FR"));
            Assert.IsFalse(c1.IsDraft("en-UK"));
            Assert.IsFalse(c1.IsDraft("dk-DA"));
            Assert.IsFalse(c1.IsDraft("de-DE"));

            // and only those with published name, are published
            Assert.IsTrue(c1.IsPublished("fr-FR"));
            Assert.IsTrue(c1.IsPublished("en-UK"));
            Assert.IsFalse(c1.IsDraft("dk-DA"));
            Assert.IsTrue(c1.IsPublished("de-DE"));

            // get the draft published content
            var c2 = s.Content.GetById(true, 1);

            // draft content = we have drafts
            Assert.IsTrue(c2.IsDraft("fr-FR"));
            Assert.IsTrue(c2.IsDraft("en-UK"));
            Assert.IsTrue(c2.IsDraft("dk-DA"));
            Assert.IsFalse(c2.IsDraft("de-DE")); // except for the one that does not

            // and only those with published name, are published
            Assert.IsTrue(c2.IsPublished("fr-FR"));
            Assert.IsTrue(c2.IsPublished("en-UK"));
            Assert.IsFalse(c2.IsPublished("dk-DA"));
            Assert.IsTrue(c2.IsPublished("de-DE"));
        }
    }
}
