using System.IO.Abstractions;
using Kavita.API.Database;
using Kavita.API.Repositories;
using Kavita.API.Services;
using Kavita.API.Services.Scanner;
using Kavita.Models.DTOs;
using Kavita.Models.DTOs.KavitaPlus.Metadata;
using Kavita.Models.Entities;
using Kavita.Models.Entities.Enums;
using Kavita.Models.Entities.MetadataMatching;
using Microsoft.Extensions.Logging;
using NSubstitute;

namespace Kavita.Services.Tests;

public class SettingsServiceTests
{
    private readonly ISettingsService _settingsService;
    private readonly IUnitOfWork _mockUnitOfWork;

    private const string DefaultAgeKey = "default_age";
    private const string DefaultFieldSource = "default_source";
    private readonly static AgeRating DefaultAgeRating = AgeRating.Everyone;
    private readonly static MetadataFieldType DefaultSourceField = MetadataFieldType.Genre;

    public SettingsServiceTests()
    {
        var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new FileSystem());

        _mockUnitOfWork = Substitute.For<IUnitOfWork>();
        _settingsService = new SettingsService(_mockUnitOfWork, ds,
            Substitute.For<ILibraryWatcher>(), Substitute.For<ITaskScheduler>(),
            Substitute.For<ILogger<SettingsService>>(), Substitute.For<IOidcService>(),
            Substitute.For<ILoggingService>());
    }

    #region ImportMetadataSettings

    [Fact]
    public async Task ImportFieldMappings_ReplaceMode()
    {
        var existingSettings = CreateDefaultMetadataSettingsDto();
        var newSettings = new MetadataSettingsDto
        {
            Whitelist = ["new_whitelist_item"],
            Blacklist = ["new_blacklist_item"],
            AgeRatingMappings = new Dictionary<string, AgeRating> { ["new_age"] = AgeRating.R18Plus },
            FieldMappings =
            [
                new MetadataFieldMappingDto { Id = 10, SourceValue = "new_source", SourceType = MetadataFieldType.Genre, DestinationValue = "new_dest", DestinationType = MetadataFieldType.Tag }
            ],
        };

        var importSettings = new ImportSettingsDto
        {
            ImportMode = ImportMode.Replace,
            Whitelist = true,
            Blacklist = true,
            AgeRatings = true,
            FieldMappings = true,
            Resolution = ConflictResolution.Manual,
            AgeRatingConflictResolutions = [],
        };

        var settingsRepo = Substitute.For<ISettingsRepository>();
        settingsRepo.GetMetadataSettingDto().Returns(existingSettings);
        _mockUnitOfWork.SettingsRepository.Returns(settingsRepo);

        var result = await _settingsService.ImportFieldMappings(newSettings, importSettings);

        Assert.True(result.Success);
        Assert.Empty(result.AgeRatingConflicts);

        Assert.Equal(existingSettings.Whitelist, newSettings.Whitelist);
        Assert.Equal(existingSettings.Blacklist, newSettings.Blacklist);
        Assert.Equal(existingSettings.AgeRatingMappings, newSettings.AgeRatingMappings);
        Assert.Equal(existingSettings.FieldMappings, newSettings.FieldMappings);
    }

    [Fact]
    public async Task ImportFieldMappings_MergeMode_WithNoConflicts()
    {
        var existingSettingsDto = CreateDefaultMetadataSettingsDto();
        var existingSettings = CreateDefaultMetadataSettings();

        var newSettings = new MetadataSettingsDto
        {
            Whitelist = ["new_whitelist_item"],
            Blacklist = ["new_blacklist_item"],
            AgeRatingMappings = new Dictionary<string, AgeRating> { ["new_age"] = AgeRating.R18Plus },
            FieldMappings =
            [
                new MetadataFieldMappingDto { Id = 10, SourceValue = "new_source", SourceType = MetadataFieldType.Genre, DestinationValue = "new_dest", DestinationType = MetadataFieldType.Tag },
            ],
        };

        var importSettings = new ImportSettingsDto
        {
            ImportMode = ImportMode.Merge,
            Whitelist = true,
            Blacklist = true,
            AgeRatings = true,
            FieldMappings = true,
            Resolution = ConflictResolution.Manual,
            AgeRatingConflictResolutions = [],
        };

        var settingsRepo = Substitute.For<ISettingsRepository>();
        settingsRepo.GetMetadataSettingDto().Returns(existingSettingsDto);
        settingsRepo.GetMetadataSettings().Returns(existingSettings);
        _mockUnitOfWork.SettingsRepository.Returns(settingsRepo);

        var result = await _settingsService.ImportFieldMappings(newSettings, importSettings);

        Assert.True(result.Success);
        Assert.Empty(result.AgeRatingConflicts);

        Assert.Contains("default_white", existingSettingsDto.Whitelist);
        Assert.Contains("new_whitelist_item", existingSettingsDto.Whitelist);
        Assert.Contains("default_black", existingSettingsDto.Blacklist);
        Assert.Contains("new_blacklist_item", existingSettingsDto.Blacklist);
        Assert.Equal(2, existingSettingsDto.AgeRatingMappings.Count);
        Assert.Equal(2, existingSettingsDto.FieldMappings.Count);
    }

    [Fact]
    public async Task ImportFieldMappings_MergeMode_UseConfiguredOverrides()
    {
        var existingSettingsDto = CreateDefaultMetadataSettingsDto();
        var existingSettings = CreateDefaultMetadataSettings();

        var newSettings = new MetadataSettingsDto
        {
            Whitelist = [],
            Blacklist = [],
            AgeRatingMappings = new Dictionary<string, AgeRating> { [DefaultAgeKey] = AgeRating.R18Plus },
            FieldMappings =
            [
                new MetadataFieldMappingDto
                {
                    Id = 20,
                    SourceValue = DefaultFieldSource,
                    SourceType = DefaultSourceField,
                    DestinationValue = "different_dest",
                    DestinationType = MetadataFieldType.Genre,
                }
            ],
        };

        var importSettings = new ImportSettingsDto
        {
            ImportMode = ImportMode.Merge,
            Whitelist = false,
            Blacklist = false,
            AgeRatings = true,
            FieldMappings = true,
            Resolution = ConflictResolution.Manual,
            AgeRatingConflictResolutions = new Dictionary<string, ConflictResolution> { [DefaultAgeKey] = ConflictResolution.Replace },
        };

        var settingsRepo = Substitute.For<ISettingsRepository>();
        settingsRepo.GetMetadataSettingDto().Returns(existingSettingsDto);
        settingsRepo.GetMetadataSettings().Returns(existingSettings);
        _mockUnitOfWork.SettingsRepository.Returns(settingsRepo);

        var result = await _settingsService.ImportFieldMappings(newSettings, importSettings);

        Assert.True(result.Success);
        Assert.Empty(result.AgeRatingConflicts);

        Assert.Equal(AgeRating.R18Plus, existingSettingsDto.AgeRatingMappings[DefaultAgeKey]);
    }

    [Fact]
    public async Task ImportFieldMappings_MergeMode_SkipIdenticalMappings()
    {
        var existingSettingsDto = CreateDefaultMetadataSettingsDto();
        var existingSettings = CreateDefaultMetadataSettings();

        var newSettings = new MetadataSettingsDto
        {
            Whitelist = [],
            Blacklist = [],
            AgeRatingMappings = new Dictionary<string, AgeRating> { ["existing_age"] = AgeRating.Mature }, // Same value
            FieldMappings =
            [
                new MetadataFieldMappingDto
                {
                    Id = 20,
                    SourceValue = "existing_source",
                    SourceType = MetadataFieldType.Genre,
                    DestinationValue = "existing_dest", // Same destination
                    DestinationType = MetadataFieldType.Tag // Same destination type
                }
            ],
        };

        var importSettings = new ImportSettingsDto
        {
            ImportMode = ImportMode.Merge,
            Whitelist = false,
            Blacklist = false,
            AgeRatings = true,
            FieldMappings = true,
            Resolution = ConflictResolution.Manual,
            AgeRatingConflictResolutions = [],
        };

        var settingsRepo = Substitute.For<ISettingsRepository>();
        settingsRepo.GetMetadataSettingDto().Returns(existingSettingsDto);
        settingsRepo.GetMetadataSettings().Returns(existingSettings);
        _mockUnitOfWork.SettingsRepository.Returns(settingsRepo);

        var result = await _settingsService.ImportFieldMappings(newSettings, importSettings);

        Assert.True(result.Success);
        Assert.Empty(result.AgeRatingConflicts);
    }


    #endregion

    #region UpdateMetadataSettings

    [Fact]
    public async Task UpdateMetadataSettings_ShouldUpdateExistingSettings()
    {

        var existingSettings = new MetadataSettings
        {
            Id = 1,
            Enabled = false,
            EnableSummary = false,
            EnableLocalizedName = false,
            EnablePublicationStatus = false,
            EnableRelationships = false,
            EnablePeople = false,
            EnableStartDate = false,
            EnableGenres = false,
            EnableTags = false,
            FirstLastPeopleNaming = false,
            EnableCoverImage = false,
            AgeRatingMappings = new Dictionary<string, AgeRating>(),
            Blacklist = [],
            Whitelist = [],
            Overrides = [],
            PersonRoles = [],
            FieldMappings = []
        };

        var settingsRepo = Substitute.For<ISettingsRepository>();
        settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings));
        settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto()));
        _mockUnitOfWork.SettingsRepository.Returns(settingsRepo);

        var updateDto = new MetadataSettingsDto
        {
            Enabled = true,
            EnableSummary = true,
            EnableLocalizedName = true,
            EnablePublicationStatus = true,
            EnableRelationships = true,
            EnablePeople = true,
            EnableStartDate = true,
            EnableGenres = true,
            EnableTags = true,
            FirstLastPeopleNaming = true,
            EnableCoverImage = true,
            AgeRatingMappings = new Dictionary<string, AgeRating> { { "Adult", AgeRating.R18Plus } },
            Blacklist = ["blacklisted-tag"],
            Whitelist = ["whitelisted-tag"],
            Overrides = [MetadataSettingField.Summary],
            PersonRoles = [PersonRole.Writer],
            FieldMappings =
            [
                new MetadataFieldMappingDto
                {
                    SourceType = MetadataFieldType.Genre,
                    DestinationType = MetadataFieldType.Tag,
                    SourceValue = "Action",
                    DestinationValue = "Fight",
                    ExcludeFromSource = true
                }
            ]
        };

        // Act
        await _settingsService.UpdateMetadataSettings(updateDto);

        // Assert
        await _mockUnitOfWork.Received(1).CommitAsync();

        // Verify properties were updated
        Assert.True(existingSettings.Enabled);
        Assert.True(existingSettings.EnableSummary);
        Assert.True(existingSettings.EnableLocalizedName);
        Assert.True(existingSettings.EnablePublicationStatus);
        Assert.True(existingSettings.EnableRelationships);
        Assert.True(existingSettings.EnablePeople);
        Assert.True(existingSettings.EnableStartDate);
        Assert.True(existingSettings.EnableGenres);
        Assert.True(existingSettings.EnableTags);
        Assert.True(existingSettings.FirstLastPeopleNaming);
        Assert.True(existingSettings.EnableCoverImage);

        // Verify collections were updated
        Assert.Single(existingSettings.AgeRatingMappings);
        Assert.Equal(AgeRating.R18Plus, existingSettings.AgeRatingMappings["Adult"]);

        Assert.Single(existingSettings.Blacklist);
        Assert.Equal("blacklisted-tag", existingSettings.Blacklist[0]);

        Assert.Single(existingSettings.Whitelist);
        Assert.Equal("whitelisted-tag", existingSettings.Whitelist[0]);

        Assert.Single(existingSettings.Overrides);
        Assert.Equal(MetadataSettingField.Summary, existingSettings.Overrides[0]);

        Assert.Single(existingSettings.PersonRoles);
        Assert.Equal(PersonRole.Writer, existingSettings.PersonRoles[0]);

        Assert.Single(existingSettings.FieldMappings);
        Assert.Equal(MetadataFieldType.Genre, existingSettings.FieldMappings[0].SourceType);
        Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[0].DestinationType);
        Assert.Equal("Action", existingSettings.FieldMappings[0].SourceValue);
        Assert.Equal("Fight", existingSettings.FieldMappings[0].DestinationValue);
        Assert.True(existingSettings.FieldMappings[0].ExcludeFromSource);
    }

    [Fact]
    public async Task UpdateMetadataSettings_WithNullCollections_ShouldUseEmptyCollections()
    {

        var existingSettings = new MetadataSettings
        {
            Id = 1,
            FieldMappings = [new MetadataFieldMapping {Id = 1, SourceValue = "OldValue"}]
        };

        var settingsRepo = Substitute.For<ISettingsRepository>();
        settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings));
        settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto()));
        _mockUnitOfWork.SettingsRepository.Returns(settingsRepo);

        var updateDto = new MetadataSettingsDto
        {
            AgeRatingMappings = null,
            Blacklist = null,
            Whitelist = null,
            Overrides = null,
            PersonRoles = null,
            FieldMappings = null
        };

        // Act
        await _settingsService.UpdateMetadataSettings(updateDto);

        // Assert
        await _mockUnitOfWork.Received(1).CommitAsync();

        Assert.Empty(existingSettings.AgeRatingMappings);
        Assert.Empty(existingSettings.Blacklist);
        Assert.Empty(existingSettings.Whitelist);
        Assert.Empty(existingSettings.Overrides);
        Assert.Empty(existingSettings.PersonRoles);

        // Verify existing field mappings were cleared
        settingsRepo.Received(1).RemoveRange(Arg.Any<List<MetadataFieldMapping>>());
        Assert.Empty(existingSettings.FieldMappings);
    }

    [Fact]
    public async Task UpdateMetadataSettings_WithFieldMappings_ShouldReplaceExistingMappings()
    {

        var existingSettings = new MetadataSettings
        {
            Id = 1,
            FieldMappings =
            [
                new MetadataFieldMapping
                {
                    Id = 1,
                    SourceType = MetadataFieldType.Genre,
                    DestinationType = MetadataFieldType.Genre,
                    SourceValue = "OldValue",
                    DestinationValue = "OldDestination",
                    ExcludeFromSource = false
                }
            ]
        };

        var settingsRepo = Substitute.For<ISettingsRepository>();
        settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings));
        settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto()));
        _mockUnitOfWork.SettingsRepository.Returns(settingsRepo);

        var updateDto = new MetadataSettingsDto
        {
            FieldMappings =
            [
                new MetadataFieldMappingDto
                {
                    SourceType = MetadataFieldType.Tag,
                    DestinationType = MetadataFieldType.Genre,
                    SourceValue = "NewValue",
                    DestinationValue = "NewDestination",
                    ExcludeFromSource = true
                },

                new MetadataFieldMappingDto
                {
                    SourceType = MetadataFieldType.Tag,
                    DestinationType = MetadataFieldType.Tag,
                    SourceValue = "AnotherValue",
                    DestinationValue = "AnotherDestination",
                    ExcludeFromSource = false
                }
            ]
        };

        // Act
        await _settingsService.UpdateMetadataSettings(updateDto);

        // Assert
        await _mockUnitOfWork.Received(1).CommitAsync();

        // Verify existing field mappings were cleared and new ones added
        settingsRepo.Received(1).RemoveRange(Arg.Any<List<MetadataFieldMapping>>());
        Assert.Equal(2, existingSettings.FieldMappings.Count);

        // Verify first mapping
        Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[0].SourceType);
        Assert.Equal(MetadataFieldType.Genre, existingSettings.FieldMappings[0].DestinationType);
        Assert.Equal("NewValue", existingSettings.FieldMappings[0].SourceValue);
        Assert.Equal("NewDestination", existingSettings.FieldMappings[0].DestinationValue);
        Assert.True(existingSettings.FieldMappings[0].ExcludeFromSource);

        // Verify second mapping
        Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[1].SourceType);
        Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[1].DestinationType);
        Assert.Equal("AnotherValue", existingSettings.FieldMappings[1].SourceValue);
        Assert.Equal("AnotherDestination", existingSettings.FieldMappings[1].DestinationValue);
        Assert.False(existingSettings.FieldMappings[1].ExcludeFromSource);
    }

    [Fact]
    public async Task UpdateMetadataSettings_WithBlacklistWhitelist_ShouldNormalizeAndDeduplicateEntries()
    {

        var existingSettings = new MetadataSettings
        {
            Id = 1,
            Blacklist = [],
            Whitelist = []
        };

        // We need to mock the repository and provide a custom implementation for ToNormalized
        var settingsRepo = Substitute.For<ISettingsRepository>();
        settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings));
        settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto()));
        _mockUnitOfWork.SettingsRepository.Returns(settingsRepo);

        var updateDto = new MetadataSettingsDto
        {
            // Include duplicates with different casing and whitespace
            Blacklist = ["tag1", "Tag1", " tag2 ", "", "  ", "tag3"],
            Whitelist = ["allowed1", "Allowed1", " allowed2 ", "", "allowed3"]
        };

        // Act
        await _settingsService.UpdateMetadataSettings(updateDto);

        // Assert
        await _mockUnitOfWork.Received(1).CommitAsync();

        Assert.Equal(3, existingSettings.Blacklist.Count);
        Assert.Equal(3, existingSettings.Whitelist.Count);
    }

    #endregion

    private MetadataSettingsDto CreateDefaultMetadataSettingsDto()
    {
        return new MetadataSettingsDto
        {
            Whitelist = ["default_white"],
            Blacklist = ["default_black"],
            AgeRatingMappings = new Dictionary<string, AgeRating> { ["default_age"] = AgeRating.Everyone },
            FieldMappings =
            [
                new MetadataFieldMappingDto
                {
                    Id = 1,
                    SourceValue = "default_source",
                    SourceType = MetadataFieldType.Genre,
                    DestinationValue = "default_dest",
                    DestinationType = MetadataFieldType.Tag
                },
            ],
        };
    }

    private MetadataSettings CreateDefaultMetadataSettings()
    {
        return new MetadataSettings
        {
            Whitelist = ["default_white"],
            Blacklist = ["default_black"],
            AgeRatingMappings = new Dictionary<string, AgeRating> { [DefaultAgeKey] = DefaultAgeRating },
            FieldMappings =
            [
                new MetadataFieldMapping
                {
                    Id = 1,
                    SourceValue = DefaultFieldSource,
                    SourceType = DefaultSourceField,
                    DestinationValue = "default_dest",
                    DestinationType = MetadataFieldType.Tag
                },
            ],
        };
    }
}
