using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Repositories.Interfaces.Unit;
using PARR.DAL.Context;
using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums;

namespace PARR.Core.Tests.Services.UnitFilterService;

public class UnitFilterServiceTests
{
    #region Helpers

    /// <summary>
    /// Создаёт тестовую сущность Unit с заданным значением поля.
    /// Используется для подготовки данных в тестах фильтрации.
    /// </summary>
    /// <param name="id">Уникальный идентификатор юнита</param>
    /// <param name="name">Имя юнита для отладки</param>
    /// <param name="fieldId">Идентификатор поля, к которому привязывается значение</param>
    /// <param name="fieldValue">Значение поля</param>
    private static Unit BuildTestUnit(Guid id, string name, Guid fieldId, string fieldValue)
    {
        var fieldVal = new UnitFieldValue { Id = Guid.NewGuid(), Value = fieldValue };
        return new Unit
        {
            Id = id,
            Name = name,
            UnitValues = new List<UnitInValue>
            {
                new UnitInValue
                {
                    UnitId = id,
                    FieldId = fieldId,
                    ValueId = fieldVal.Id,
                    Value = fieldVal
                }
            }
        };
    }

    /// <summary>
    /// Создаёт тестовый объект JobGroupType с кодом Umbrella.
    /// Используется для тестирования логики групповых работ типа "зонтик".
    /// </summary>
    private static JobGroupType BuildTestJobGroupType() => new()
    {
        Id = Guid.NewGuid(),
        Code = JobGroupTypesEnum.Group,
        Name = "Grouped Group",
        Description = "Test"
    };

    /// <summary>
    /// Создаёт тестовый объект JobGroup с заполненными обязательными полями.
    /// Используется для подготовки навигационных свойств в тестах.
    /// </summary>
    private static JobGroup BuildTestJobGroup() => new()
    {
        Id = Guid.NewGuid(),
        GroupName = "Test Group",
        ShortDescription = "Short",
        FullDescription = "Full",
        Solution = "Sol",
        TemplateDuration = "0:00:00",
        ReferenceDate = DateTimeOffset.UtcNow,
        GroupTypeId = Guid.NewGuid(),
        ScheduleExcludeTypeId = Guid.NewGuid(),
        GroupType = BuildTestJobGroupType()
    };

    /// <summary>
    /// Создаёт тестовый объект JobAutoControl с отключённым автоконтролем.
    /// Используется для заполнения обязательного свойства в сущности Job.
    /// </summary>
    private static JobAutoControl BuildTestAutoControl(Guid jobId) => new()
    {
        JobId = jobId,
        IsEnable = false
    };

    /// <summary>
    /// Контейнер для моков репозиториев.
    /// Упрощает передачу зависимостей в метод создания сервиса.
    /// </summary>
    private class RepositoryMocks
    {
        public Mock<IUnitRepository> Unit { get; set; }
        public Mock<IUnitInUnitRepository> UnitInUnit { get; set; }
        public Mock<IUnitInValueRepository> UnitInValue { get; set; }
        public Mock<IUnitFieldRepository> UnitField { get; set; }
        public Mock<IJobRepository> Job { get; set; }
        public Mock<IRedisCacheService> Cache { get; set; }
    }

    /// <summary>
    /// Настраивает базовые моки репозиториев с использованием InMemory-контекста.
    /// Возвращает объект с подготовленными моками для повторного использования в тестах.
    /// </summary>
    /// <param name="context">Экземпляр DataContext с тестовыми данными</param>
    private static RepositoryMocks ArrangeRepositoryMocks(DataContext context)
    {
        var unitRepoMock = new Mock<IUnitRepository>();
        unitRepoMock.Setup(r => r.Get()).Returns(context.Units.AsQueryable());
        unitRepoMock.Setup(r => r.GetInitialUnitIds(It.IsAny<string>()))
                    .Returns(context.Units.Select(u => u.Id).AsQueryable());

        var unitInUnitRepoMock = new Mock<IUnitInUnitRepository>();
        unitInUnitRepoMock.Setup(r => r.Get()).Returns(context.UnitInUnits.AsQueryable());

        var unitInValueRepoMock = new Mock<IUnitInValueRepository>();
        // Единый подход: используем контекст для поддержки IAsyncQueryProvider
        // Это гарантирует корректную работу .ToListAsync() внутри сервиса
        unitInValueRepoMock.Setup(r => r.GetMatchingTargetIds(It.IsAny<Guid>(), It.IsAny<string>()))
                           .Returns((Guid fId, string mask) =>
                               context.UnitInValues
                               .AsNoTracking()
                               .Where(uv => uv.FieldId == fId && uv.Value != null)
                               .Select(uv => uv.UnitId)
                               .Distinct()
                               .AsQueryable());

        var unitFieldRepoMock = new Mock<IUnitFieldRepository>();
        unitFieldRepoMock.Setup(r => r.Get()).Returns(new List<UnitField>().AsQueryable());

        var jobRepoMock = new Mock<IJobRepository>();
        jobRepoMock.Setup(r => r.Get()).Returns(context.Jobs.AsQueryable());

        var cacheMock = new Mock<IRedisCacheService>();
        cacheMock.Setup(r => r.GetCachedDataAsync<UnitFilterIds>(It.IsAny<string>(), It.IsAny<bool>()))
                 .ReturnsAsync((UnitFilterIds)null!);

        return new RepositoryMocks
        {
            Unit = unitRepoMock,
            UnitInUnit = unitInUnitRepoMock,
            UnitInValue = unitInValueRepoMock,
            UnitField = unitFieldRepoMock,
            Job = jobRepoMock,
            Cache = cacheMock
        };
    }

    /// <summary>
    /// Создаёт экземпляр тестируемого сервиса (SUT — System Under Test).
    /// Инкапсулирует логику конструктора для упрощения тестов.
    /// </summary>
    /// <param name="mocks">Подготовленные моки репозиториев</param>
    /// <param name="logger">Экземпляр логгера для сервиса</param>
    private static PARR.Core.Services.UnitFilterService.UnitFilterService CreateSut(
        RepositoryMocks mocks,
        ILogger<PARR.Core.Services.UnitFilterService.UnitFilterService> logger)
    {
        var serviceOptions = Options.Create(new PARR.Core.Services.UnitFilterService.Models.UnitFilterServiceOptions { LoadBatchSize = 100 });

        return new PARR.Core.Services.UnitFilterService.UnitFilterService(
            logger,
            mocks.Job.Object,
            mocks.Unit.Object,
            mocks.UnitInUnit.Object,
            mocks.UnitInValue.Object,
            mocks.Cache.Object,
            serviceOptions,
            mocks.UnitField.Object
        );
    }

    #endregion


    /// <summary>
    /// Проверяет, что юниты, не прошедшие фильтрацию по полю (FieldFilter) 
    /// или по связям (RelationshipFilter), корректно исключаются из результата.
    /// </summary>
    [Fact]
    public async Task GetUnitsByJobFilterAsync_UnitsWithInvalidFieldOrRelationshipValue_AreExcluded()
    {
        // === Arrange ===
        var dbOptions = new DbContextOptionsBuilder<DataContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
        using var context = new DataContext(dbOptions);

        var jobId = Guid.NewGuid();
        var fieldId = Guid.NewGuid();
        var validParentId = Guid.NewGuid();
        var wrongParentId = Guid.NewGuid();

        var unitPassesAll = Guid.NewGuid();
        var unitFailsFieldFilter = Guid.NewGuid();
        var unitFailsRelationshipFilter = Guid.NewGuid();

        // Подготовка тестовых данных: юниты
        context.Units.AddRange(
            BuildTestUnit(unitPassesAll, "Unit_PassesAll", fieldId, "Accepted Value"),
            BuildTestUnit(unitFailsFieldFilter, "Unit_FailsField", fieldId, "Rejected Value"),
            BuildTestUnit(unitFailsRelationshipFilter, "Unit_FailsRel", fieldId, "Accepted Value"),
            BuildTestUnit(validParentId, "Parent_Valid", fieldId, "Valid Parent"),
            BuildTestUnit(wrongParentId, "Parent_Wrong", fieldId, "Wrong Parent")
        );

        // Подготовка тестовых данных: связи между юнитами
        context.UnitInUnits.AddRange(
            new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitPassesAll, DateCreated = DateTimeOffset.UtcNow },
            new UnitInUnit { ParentUnitId = wrongParentId, ChildUnitId = unitFailsRelationshipFilter, DateCreated = DateTimeOffset.UtcNow }
        );

        await context.SaveChangesAsync();

        // Формируем тестовый объект Job с фильтрами
        var job = new Job
        {
            Id = jobId,
            Name = "Test Job",
            WorkName = "Test Work",
            TemplateNameMask = "TestMask",
            WorkGroupMask = "TestGroup",
            ResponseAreaMask = "TestArea",
            GroupId = Guid.NewGuid(),
            TnkId = Guid.NewGuid(),
            MinValueRelationships = 1,
            MaxValueRelationships = 10,
            IsParentRelationships = true,
            AutoControl = BuildTestAutoControl(jobId),
            Group = BuildTestJobGroup(),
            UnitFilters = new List<JobUnitFilter>
            {
                new JobUnitFilter
                {
                    Id = Guid.NewGuid(),
                    JobId = jobId,
                    UnitFilter = "%",
                    FieldFilters = new List<JobFieldFilter>
                    {
                        new JobFieldFilter { Id = Guid.NewGuid(), FieldId = fieldId, ValueMask = "Accepted%", IsInverse = false }
                    },
                    RelationshipFilters = new List<JobRelationshipFilter>
                    {
                        new JobRelationshipFilter { UnitFilterId = Guid.NewGuid(), FieldId = fieldId, ValueMask = "%", IsParent = true, IsInverse = false, IsFullMatch = false }
                    }
                }
            }
        };

        // Настройка моков репозиториев
        var mocks = ArrangeRepositoryMocks(context);

        // Переопределяем мок фильтрации по полю: в реальном коде используется EF.Functions.ILike,
        // в тесте заменяем на прямое сравнение строк для предсказуемости
        mocks.Unit.Setup(r => r.GetUnitByFieldAndValue(It.IsAny<IQueryable<Unit>>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<bool>()))
                    .Returns((IQueryable<Unit> q, Guid fId, string mask, bool inv) =>
                        q.Where(u => u.UnitValues != null && u.UnitValues.Any(v => v.FieldId == fId && v.Value != null && v.Value.Value == "Accepted Value")));

        // ИСПРАВЛЕНО: используем запрос к контексту вместо массива.
        // Массивный AsQueryable() не поддерживает IAsyncQueryProvider, что вызывает крах при вызове .ToListAsync() внутри сервиса.
        // Запрос к InMemory DbSet гарантирует корректную асинхронную материализацию.
        mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(fieldId, It.IsAny<string>()))
                         .Returns(context.Units.Where(u => u.Id == validParentId).Select(u => u.Id).AsQueryable());

        var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
        var service = CreateSut(mocks, logger);

        // === Act ===
        var result = await service.GetUnitsByJobFilterAsync(job);

        // === Assert ===
        var resultIds = result.Select(u => u.Id).ToList();

        Assert.Contains(unitPassesAll, resultIds);
        Assert.DoesNotContain(unitFailsFieldFilter, resultIds);
        Assert.DoesNotContain(unitFailsRelationshipFilter, resultIds);

        Assert.Single(result);
        var dto = result.First();
        Assert.Equal("Accepted Value", dto.Values.FirstOrDefault()?.Value);
        Assert.Single(dto.Parents);
        Assert.Equal(validParentId, dto.Parents.First().UnitId);
    }

    /// <summary>
    /// Проверяет, что юниты исключаются, если их единственный родительский юнит 
    /// содержит запрещённое значение тега (сценарий с IsInverse = true).
    /// </summary>
    [Fact]
    public async Task GetUnitsByJobFilterAsync_ParentWithForbiddenTag_UnitIsExcluded()
    {
        // === Arrange ===
        var dbOptions = new DbContextOptionsBuilder<DataContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
        using var context = new DataContext(dbOptions);

        var jobId = Guid.NewGuid();
        var tagFieldId = Guid.NewGuid();
        var validParentId = Guid.NewGuid();
        var invalidParentId = Guid.NewGuid();
        var unitWithValidParent = Guid.NewGuid();
        var unitWithInvalidParent = Guid.NewGuid();

        // Подготовка тестовых данных: юниты
        context.Units.AddRange(
            BuildTestUnit(validParentId, "Parent_Valid", tagFieldId, "ОТВ.ЭК"),
            BuildTestUnit(invalidParentId, "Parent_Invalid", tagFieldId, "ПАРР-РРПТК-ОТВ.ЭК"),
            BuildTestUnit(unitWithValidParent, "Unit_Valid", Guid.NewGuid(), "Val"),
            BuildTestUnit(unitWithInvalidParent, "Unit_Invalid", Guid.NewGuid(), "Val")
        );

        // Подготовка тестовых данных: связи между юнитами
        context.UnitInUnits.AddRange(
            new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitWithValidParent, DateCreated = DateTimeOffset.UtcNow },
            new UnitInUnit { ParentUnitId = invalidParentId, ChildUnitId = unitWithInvalidParent, DateCreated = DateTimeOffset.UtcNow }
        );

        await context.SaveChangesAsync();

        // Формируем тестовый объект Job с настройкой IsInverse = true
        // IsInverse = true означает: исключить родителей, которые СОВПАДАЮТ с маской
        var job = new Job
        {
            Id = jobId,
            Name = "Test Job Inverse",
            WorkName = "Test Work",
            TemplateNameMask = "TestMask",
            WorkGroupMask = "TestGroup",
            ResponseAreaMask = "TestArea",
            GroupId = Guid.NewGuid(),
            TnkId = Guid.NewGuid(),
            MinValueRelationships = 1,
            MaxValueRelationships = 10,
            IsParentRelationships = true,
            AutoControl = BuildTestAutoControl(jobId),
            Group = BuildTestJobGroup(),
            UnitFilters = new List<JobUnitFilter>
            {
                new JobUnitFilter
                {
                    Id = Guid.NewGuid(),
                    JobId = jobId,
                    UnitFilter = "%",
                    FieldFilters = new List<JobFieldFilter>(),
                    RelationshipFilters = new List<JobRelationshipFilter>
                    {
                        new JobRelationshipFilter
                        {
                            UnitFilterId = Guid.NewGuid(),
                            FieldId = tagFieldId,
                            ValueMask = "%ПАРР-РРПТК-ОТВ.ЭК%",
                            IsParent = true,
                            IsInverse = true,
                            IsFullMatch = false
                        }
                    }
                }
            }
        };

        // Настройка моков репозиториев
        var mocks = ArrangeRepositoryMocks(context);

        // Переопределяем мок: возвращаем только родителей с запрещённым тегом.
        // В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
        // IsInverse = true в сервисе инвертирует результат, поэтому эти родители будут исключены.
        mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny<string>()))
                             .Returns((Guid fId, string mask) =>
                                 context.UnitInValues
                                 .AsNoTracking()
                                 .Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("ПАРР-РРПТК-ОТВ.ЭК", StringComparison.OrdinalIgnoreCase))
                                 .Select(uv => uv.UnitId)
                                 .Distinct()
                                 .AsQueryable());

        var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
        var service = CreateSut(mocks, logger);

        // === Act ===
        var result = await service.GetUnitsByJobFilterAsync(job);

        // === Assert ===
        var resultIds = result.Select(u => u.Id).ToList();

        Assert.Contains(unitWithValidParent, resultIds);
        Assert.DoesNotContain(unitWithInvalidParent, resultIds);
    }

    /// <summary>
    /// Проверяет логику IsFullMatch = true: юнит исключается, 
    /// если хотя бы один из его родительских юнитов не соответствует фильтру.
    /// </summary>
    [Fact]
    public async Task GetUnitsByJobFilterAsync_IsFullMatch_UnitWithAnyNonMatchingParent_IsExcluded()
    {
        // === Arrange ===
        var dbOptions = new DbContextOptionsBuilder<DataContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
        using var context = new DataContext(dbOptions);

        var jobId = Guid.NewGuid();
        var checkFieldId = Guid.NewGuid();
        var parentGoodId = Guid.NewGuid();
        var parentBadId = Guid.NewGuid();
        var unitMixedId = Guid.NewGuid();
        var unitCleanId = Guid.NewGuid();

        // Подготовка тестовых данных: юниты
        context.Units.AddRange(
            BuildTestUnit(parentGoodId, "Parent_Good", checkFieldId, "Good Parent"),
            BuildTestUnit(parentBadId, "Parent_Bad", checkFieldId, "Bad Parent"),
            BuildTestUnit(unitMixedId, "Unit_Mixed", Guid.NewGuid(), "Mixed"),
            BuildTestUnit(unitCleanId, "Unit_Clean", Guid.NewGuid(), "Clean")
        );

        // Подготовка тестовых данных: связи между юнитами
        context.UnitInUnits.AddRange(
            new UnitInUnit { ParentUnitId = parentGoodId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
            new UnitInUnit { ParentUnitId = parentBadId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
            new UnitInUnit { ParentUnitId = parentGoodId, ChildUnitId = unitCleanId, DateCreated = DateTimeOffset.UtcNow }
        );

        await context.SaveChangesAsync();

        // Формируем тестовый объект Job с настройкой IsFullMatch = true
        // IsFullMatch = true означает: ВСЕ родители должны соответствовать фильтру
        var job = new Job
        {
            Id = jobId,
            Name = "Test Job FullMatch",
            WorkName = "Test Work",
            TemplateNameMask = "TestMask",
            WorkGroupMask = "TestGroup",
            ResponseAreaMask = "TestArea",
            GroupId = Guid.NewGuid(),
            TnkId = Guid.NewGuid(),
            MinValueRelationships = 1,
            MaxValueRelationships = 10,
            IsParentRelationships = true,
            AutoControl = BuildTestAutoControl(jobId),
            Group = BuildTestJobGroup(),
            UnitFilters = new List<JobUnitFilter>
            {
                new JobUnitFilter
                {
                    Id = Guid.NewGuid(),
                    JobId = jobId,
                    UnitFilter = "%",
                    FieldFilters = new List<JobFieldFilter>(),
                    RelationshipFilters = new List<JobRelationshipFilter>
                    {
                        new JobRelationshipFilter
                        {
                            UnitFilterId = Guid.NewGuid(),
                            FieldId = checkFieldId,
                            ValueMask = "%Good%",
                            IsParent = true,
                            IsInverse = false,
                            IsFullMatch = true
                        }
                    }
                }
            }
        };

        // Настройка моков репозиториев
        var mocks = ArrangeRepositoryMocks(context);

        // Переопределяем мок: возвращаем только родителей, соответствующих маске "Good".
        // В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
        mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(checkFieldId, It.IsAny<string>()))
                             .Returns((Guid fId, string mask) =>
                                 context.UnitInValues
                                 .AsNoTracking()
                                 .Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("Good", StringComparison.OrdinalIgnoreCase))
                                 .Select(uv => uv.UnitId)
                                 .Distinct()
                                 .AsQueryable());

        var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
        var service = CreateSut(mocks, logger);

        // === Act ===
        var result = await service.GetUnitsByJobFilterAsync(job);

        // === Assert ===
        var resultIds = result.Select(u => u.Id).ToList();

        Assert.Contains(unitCleanId, resultIds);
        Assert.DoesNotContain(unitMixedId, resultIds);
    }


    /// <summary>
    /// Проверяет комбинацию флагов: IsParent=true, IsInverse=true, IsFullMatch=true.
    /// Логика: юнит проходит, только если НИ ОДИН из его родителей не содержит запрещённое значение.
    /// </summary>
    [Fact]
    public async Task GetUnitsByJobFilterAsync_IsParentAndInverseAndFullMatch_UnitWithAnyForbiddenParent_IsExcluded()
    {
        // === Arrange ===
        var dbOptions = new DbContextOptionsBuilder<DataContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
        using var context = new DataContext(dbOptions);

        var jobId = Guid.NewGuid();
        var tagFieldId = Guid.NewGuid();
        var parentForbiddenId = Guid.NewGuid(); // Родитель с запрещённым тегом
        var parentCleanId = Guid.NewGuid();     // Родитель без запрещённого тега
        var unitMixedId = Guid.NewGuid();       // Юнит с одним "плохим" и одним "хорошим" родителем
        var unitAllCleanId = Guid.NewGuid();    // Юнит только с "хорошими" родителями

        // Подготовка тестовых данных: юниты
        context.Units.AddRange(
            BuildTestUnit(parentForbiddenId, "Parent_Forbidden", tagFieldId, "FORBIDDEN-TAG"),
            BuildTestUnit(parentCleanId, "Parent_Clean", tagFieldId, "CLEAN-TAG"),
            BuildTestUnit(unitMixedId, "Unit_Mixed", Guid.NewGuid(), "Mixed"),
            BuildTestUnit(unitAllCleanId, "Unit_AllClean", Guid.NewGuid(), "AllClean")
        );

        // Подготовка тестовых данных: связи
        // unitMixed имеет обоих родителей — один с запрещённым тегом, один без
        // unitAllClean имеет только "чистого" родителя
        context.UnitInUnits.AddRange(
            new UnitInUnit { ParentUnitId = parentForbiddenId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
            new UnitInUnit { ParentUnitId = parentCleanId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
            new UnitInUnit { ParentUnitId = parentCleanId, ChildUnitId = unitAllCleanId, DateCreated = DateTimeOffset.UtcNow }
        );

        await context.SaveChangesAsync();

        // Формируем тестовый объект Job с комбинацией флагов:
        // IsParent = true: проверяем родительские связи
        // IsInverse = true: исключаем родителей, которые СОВПАДАЮТ с маской
        // IsFullMatch = true: ВСЕ родители должны пройти фильтр (ни один не должен совпасть с маской)
        var job = new Job
        {
            Id = jobId,
            Name = "Test Job Combined Flags",
            WorkName = "Test Work",
            TemplateNameMask = "TestMask",
            WorkGroupMask = "TestGroup",
            ResponseAreaMask = "TestArea",
            GroupId = Guid.NewGuid(),
            TnkId = Guid.NewGuid(),
            MinValueRelationships = 1,
            MaxValueRelationships = 10,
            IsParentRelationships = true,
            AutoControl = BuildTestAutoControl(jobId),
            Group = BuildTestJobGroup(),
            UnitFilters = new List<JobUnitFilter>
            {
                new JobUnitFilter
                {
                    Id = Guid.NewGuid(),
                    JobId = jobId,
                    UnitFilter = "%",
                    FieldFilters = new List<JobFieldFilter>(),
                    RelationshipFilters = new List<JobRelationshipFilter>
                    {
                        new JobRelationshipFilter
                        {
                            UnitFilterId = Guid.NewGuid(),
                            FieldId = tagFieldId,
                            ValueMask = "%FORBIDDEN%",
                            IsParent = true,
                            IsInverse = true,
                            IsFullMatch = true
                        }
                    }
                }
            }
        };

        // Настройка моков репозиториев
        var mocks = ArrangeRepositoryMocks(context);

        // Переопределяем мок: возвращаем только родителей, содержащих "FORBIDDEN".
        // В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
        // Из-за IsInverse=true эти родители будут исключены из "валидных".
        // Из-за IsFullMatch=true юнит пройдёт, только если ВСЕ его родители валидны.
        mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny<string>()))
                             .Returns((Guid fId, string mask) =>
                                 context.UnitInValues
                                 .AsNoTracking()
                                 .Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("FORBIDDEN", StringComparison.OrdinalIgnoreCase))
                                 .Select(uv => uv.UnitId)
                                 .Distinct()
                                 .AsQueryable());

        var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
        var service = CreateSut(mocks, logger);

        // === Act ===
        var result = await service.GetUnitsByJobFilterAsync(job);

        // === Assert ===
        var resultIds = result.Select(u => u.Id).ToList();

        // unitAllCleanId должен остаться: у него один родитель, и он не содержит запрещённый тег
        Assert.Contains(unitAllCleanId, resultIds);

        // unitMixedId должен быть исключён: у него есть родитель с запрещённым тегом.
        // Логика: 
        // 1. GetMatchingTargetIds возвращает {parentForbiddenId}
        // 2. IsInverse=true → валидные родители = все.Кроме({parentForbiddenId}) = {parentCleanId}
        // 3. IsFullMatch=true → проверяем: все родители {parentForbiddenId, parentCleanId} входят в {parentCleanId}? Нет.
        // 4. Юнит исключается.
        Assert.DoesNotContain(unitMixedId, resultIds);
    }
}