using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Shared;
using ZB.MOM.WW.CBDD.Tests;
using Xunit;

namespace ZB.MOM.WW.CBDD.Tests;

/// <summary>
/// Tests for circular references and N-N relationships
/// Validates that the source generator handles:
/// 1. Self-referencing entities using ObjectId references (Employee → ManagerId, DirectReportIds)
/// 2. N-N via referencing with ObjectIds (CategoryRef/ProductRef) - BEST PRACTICE
/// 
/// Note: Bidirectional embedding (Category ↔ Product with full objects) is NOT supported
/// by the source generator and is an anti-pattern for document databases.
/// Use referencing (ObjectIds) instead for N-N relationships.
/// </summary>
public class CircularReferenceTests : IDisposable
{
    private readonly string _dbPath;
    private readonly Shared.TestDbContext _context;

    public CircularReferenceTests()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_circular_test_{Guid.NewGuid()}");
        _context = new Shared.TestDbContext(_dbPath);
    }

    public void Dispose()
    {
        _context?.Dispose();
        if (Directory.Exists(_dbPath))
        {
            Directory.Delete(_dbPath, true);
        }
    }

    // ========================================
    // Self-Reference Tests (Employee hierarchy with ObjectId references)
    // ========================================

    [Fact]
    public void SelfReference_InsertAndQuery_ShouldWork()
    {
        // Arrange: Create organizational hierarchy using ObjectId references
        var ceoId = ObjectId.NewObjectId();
        var manager1Id = ObjectId.NewObjectId();
        var manager2Id = ObjectId.NewObjectId();
        var developerId = ObjectId.NewObjectId();

        var ceo = new Employee
        {
            Id = ceoId,
            Name = "Alice CEO",
            Department = "Executive",
            ManagerId = null,
            DirectReportIds = new List<ObjectId> { manager1Id, manager2Id }
        };

        var manager1 = new Employee
        {
            Id = manager1Id,
            Name = "Bob Manager",
            Department = "Engineering",
            ManagerId = ceoId,
            DirectReportIds = new List<ObjectId> { developerId }
        };

        var manager2 = new Employee
        {
            Id = manager2Id,
            Name = "Carol Manager",
            Department = "Sales",
            ManagerId = ceoId,
            DirectReportIds = new List<ObjectId>() // No direct reports
        };

        var developer = new Employee
        {
            Id = developerId,
            Name = "Dave Developer",
            Department = "Engineering",
            ManagerId = manager1Id,
            DirectReportIds = null // Leaf node
        };

        // Act: Insert all employees
        _context.Employees.Insert(ceo);
        _context.Employees.Insert(manager1);
        _context.Employees.Insert(manager2);
        _context.Employees.Insert(developer);

        // Assert: Query and verify
        var queriedCeo = _context.Employees.FindById(ceoId);
        queriedCeo.ShouldNotBeNull();
        queriedCeo.Name.ShouldBe("Alice CEO");
        queriedCeo.DirectReportIds.ShouldNotBeNull();
        queriedCeo.DirectReportIds.Count.ShouldBe(2);
        queriedCeo.DirectReportIds.ShouldContain(manager1Id);
        queriedCeo.DirectReportIds.ShouldContain(manager2Id);

        // Query manager and verify direct reports
        var queriedManager1 = _context.Employees.FindById(manager1Id);
        queriedManager1.ShouldNotBeNull();
        queriedManager1.ManagerId.ShouldBe(ceoId);
        queriedManager1.DirectReportIds.ShouldNotBeNull();
        queriedManager1.DirectReportIds.Count().ShouldBe(1);
        queriedManager1.DirectReportIds.ShouldContain(developerId);

        // Query developer and verify no direct reports
        var queriedDeveloper = _context.Employees.FindById(developerId);
        queriedDeveloper.ShouldNotBeNull();
        queriedDeveloper.ManagerId.ShouldBe(manager1Id);
        // Empty list is acceptable (same as null semantically - no direct reports)
        (queriedDeveloper.DirectReportIds ?? new List<ObjectId>()).ShouldBeEmpty();
    }

    [Fact]
    public void SelfReference_UpdateDirectReports_ShouldPersist()
    {
        // Arrange: Create manager with one direct report
        var managerId = ObjectId.NewObjectId();
        var employee1Id = ObjectId.NewObjectId();
        var employee2Id = ObjectId.NewObjectId();

        var manager = new Employee
        {
            Id = managerId,
            Name = "Manager",
            Department = "Engineering",
            DirectReportIds = new List<ObjectId> { employee1Id }
        };

        var employee1 = new Employee
        {
            Id = employee1Id,
            Name = "Employee 1",
            Department = "Engineering",
            ManagerId = managerId
        };

        var employee2 = new Employee
        {
            Id = employee2Id,
            Name = "Employee 2",
            Department = "Engineering",
            ManagerId = managerId
        };

        _context.Employees.Insert(manager);
        _context.Employees.Insert(employee1);
        _context.Employees.Insert(employee2);

        // Act: Add another direct report
        manager.DirectReportIds?.Add(employee2Id);
        _context.Employees.Update(manager);

        // Assert: Verify update persisted
        var queried = _context.Employees.FindById(managerId);
        queried.ShouldNotBeNull();
        queried.DirectReportIds.ShouldNotBeNull();
        queried.DirectReportIds.Count.ShouldBe(2);
        queried.DirectReportIds.ShouldContain(employee1Id);
        queried.DirectReportIds.ShouldContain(employee2Id);
    }

    [Fact]
    public void SelfReference_QueryByManagerId_ShouldWork()
    {
        // Arrange: Create hierarchy
        var managerId = ObjectId.NewObjectId();

        var manager = new Employee
        {
            Id = managerId,
            Name = "Manager",
            Department = "Engineering"
        };

        var employee1 = new Employee
        {
            Id = ObjectId.NewObjectId(),
            Name = "Employee 1",
            Department = "Engineering",
            ManagerId = managerId
        };

        var employee2 = new Employee
        {
            Id = ObjectId.NewObjectId(),
            Name = "Employee 2",
            Department = "Engineering",
            ManagerId = managerId
        };

        _context.Employees.Insert(manager);
        _context.Employees.Insert(employee1);
        _context.Employees.Insert(employee2);

        // Act: Query all employees with specific manager
        var subordinates = _context.Employees
            .AsQueryable()
            .Where(e => e.ManagerId == managerId)
            .ToList();

        // Assert: Should find both employees
        subordinates.Count.ShouldBe(2);
        subordinates.ShouldContain(e => e.Name == "Employee 1");
        subordinates.ShouldContain(e => e.Name == "Employee 2");
    }

    // ========================================
    // N-N Referencing Tests (CategoryRef/ProductRef)
    // BEST PRACTICE for document databases
    // ========================================

    [Fact]
    public void NtoNReferencing_InsertAndQuery_ShouldWork()
    {
        // Arrange: Create categories and products with ObjectId references
        var categoryId1 = ObjectId.NewObjectId();
        var categoryId2 = ObjectId.NewObjectId();
        var productId1 = ObjectId.NewObjectId();
        var productId2 = ObjectId.NewObjectId();

        var electronics = new CategoryRef
        {
            Id = categoryId1,
            Name = "Electronics",
            Description = "Electronic devices",
            ProductIds = new List<ObjectId> { productId1, productId2 }
        };

        var computers = new CategoryRef
        {
            Id = categoryId2,
            Name = "Computers",
            Description = "Computing devices",
            ProductIds = new List<ObjectId> { productId1 }
        };

        var laptop = new ProductRef
        {
            Id = productId1,
            Name = "Laptop",
            Price = 999.99m,
            CategoryIds = new List<ObjectId> { categoryId1, categoryId2 }
        };

        var phone = new ProductRef
        {
            Id = productId2,
            Name = "Phone",
            Price = 599.99m,
            CategoryIds = new List<ObjectId> { categoryId1 }
        };

        // Act: Insert all entities
        _context.CategoryRefs.Insert(electronics);
        _context.CategoryRefs.Insert(computers);
        _context.ProductRefs.Insert(laptop);
        _context.ProductRefs.Insert(phone);

        // Assert: Query and verify references
        var queriedCategory = _context.CategoryRefs.FindById(categoryId1);
        queriedCategory.ShouldNotBeNull();
        queriedCategory.Name.ShouldBe("Electronics");
        queriedCategory.ProductIds.ShouldNotBeNull();
        queriedCategory.ProductIds.Count.ShouldBe(2);
        queriedCategory.ProductIds.ShouldContain(productId1);
        queriedCategory.ProductIds.ShouldContain(productId2);

        var queriedProduct = _context.ProductRefs.FindById(productId1);
        queriedProduct.ShouldNotBeNull();
        queriedProduct.Name.ShouldBe("Laptop");
        queriedProduct.CategoryIds.ShouldNotBeNull();
        queriedProduct.CategoryIds.Count.ShouldBe(2);
        queriedProduct.CategoryIds.ShouldContain(categoryId1);
        queriedProduct.CategoryIds.ShouldContain(categoryId2);
    }

    [Fact]
    public void NtoNReferencing_UpdateRelationships_ShouldPersist()
    {
        // Arrange: Create category and product
        var categoryId = ObjectId.NewObjectId();
        var productId1 = ObjectId.NewObjectId();
        var productId2 = ObjectId.NewObjectId();

        var category = new CategoryRef
        {
            Id = categoryId,
            Name = "Books",
            Description = "Book category",
            ProductIds = new List<ObjectId> { productId1 }
        };

        var product1 = new ProductRef
        {
            Id = productId1,
            Name = "Book 1",
            Price = 19.99m,
            CategoryIds = new List<ObjectId> { categoryId }
        };

        var product2 = new ProductRef
        {
            Id = productId2,
            Name = "Book 2",
            Price = 29.99m,
            CategoryIds = new List<ObjectId>()
        };

        _context.CategoryRefs.Insert(category);
        _context.ProductRefs.Insert(product1);
        _context.ProductRefs.Insert(product2);

        // Act: Add product2 to category
        category.ProductIds?.Add(productId2);
        _context.CategoryRefs.Update(category);

        product2.CategoryIds?.Add(categoryId);
        _context.ProductRefs.Update(product2);

        // Assert: Verify relationships updated
        var queriedCategory = _context.CategoryRefs.FindById(categoryId);
        queriedCategory.ShouldNotBeNull();
        queriedCategory.ProductIds.ShouldNotBeNull();
        queriedCategory.ProductIds.Count.ShouldBe(2);
        queriedCategory.ProductIds.ShouldContain(productId2);

        var queriedProduct2 = _context.ProductRefs.FindById(productId2);
        queriedProduct2.ShouldNotBeNull();
        queriedProduct2.CategoryIds.ShouldNotBeNull();
        queriedProduct2.CategoryIds.Count().ShouldBe(1);
        queriedProduct2.CategoryIds.ShouldContain(categoryId);
    }

    [Fact]
    public void NtoNReferencing_DocumentSize_RemainSmall()
    {
        // Arrange: Create category referencing 100 products (only IDs)
        var categoryId = ObjectId.NewObjectId();
        var productIds = Enumerable.Range(0, 100)
            .Select(_ => ObjectId.NewObjectId())
            .ToList();

        var category = new CategoryRef
        {
            Id = categoryId,
            Name = "Large Category",
            Description = "Category with 100 products",
            ProductIds = productIds
        };

        // Act: Insert and query
        _context.CategoryRefs.Insert(category);
        var queried = _context.CategoryRefs.FindById(categoryId);

        // Assert: Document remains small (only ObjectIds, no embedding)
        queried.ShouldNotBeNull();
        queried.ProductIds?.Count.ShouldBe(100);

        // Note: 100 ObjectIds = ~1.2KB (vs embedding full products = potentially hundreds of KBs)
        // This demonstrates why referencing is preferred for large N-N relationships
    }

    [Fact]
    public void NtoNReferencing_QueryByProductId_ShouldWork()
    {
        // Arrange: Create multiple categories referencing same product
        var productId = ObjectId.NewObjectId();

        var category1 = new CategoryRef
        {
            Id = ObjectId.NewObjectId(),
            Name = "Category 1",
            Description = "First category",
            ProductIds = new List<ObjectId> { productId }
        };

        var category2 = new CategoryRef
        {
            Id = ObjectId.NewObjectId(),
            Name = "Category 2",
            Description = "Second category",
            ProductIds = new List<ObjectId> { productId }
        };

        _context.CategoryRefs.Insert(category1);
        _context.CategoryRefs.Insert(category2);

        // Act: Query all categories containing the product
        var categoriesWithProduct = _context.CategoryRefs
            .AsQueryable()
            .Where(c => c.ProductIds != null && c.ProductIds.Contains(productId))
            .ToList();

        // Assert: Should find both categories
        categoriesWithProduct.Count.ShouldBe(2);
        categoriesWithProduct.ShouldContain(c => c.Name == "Category 1");
        categoriesWithProduct.ShouldContain(c => c.Name == "Category 2");
    }
}
