// EN: Custom WebApplicationFactory for functional tests with full dependency mocking.
// VI: WebApplicationFactory tùy chỉnh cho functional tests với mock đầy đủ dependencies.

using System.Data;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OrderService.API.Hubs;
using OrderService.API.Infrastructure.Tenant;
using OrderService.Domain.AggregatesModel.OrderAggregate;
using OrderService.Domain.Strategies;
using OrderService.Infrastructure;
using OrderService.Infrastructure.ExternalServices;
using Serilog;

namespace OrderService.FunctionalTests;

/// <summary>
/// EN: Custom WebApplicationFactory for functional tests.
///     Replaces PostgreSQL with InMemory database, mocks external services,
///     and provides test authentication.
/// VI: WebApplicationFactory tùy chỉnh cho functional tests.
///     Thay thế PostgreSQL bằng InMemory database, mock external services,
///     và cung cấp test authentication.
/// </summary>
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    /// <summary>
    /// EN: Unique database name per factory instance to isolate test data.
    /// VI: Tên database duy nhất cho mỗi factory instance để cách ly dữ liệu test.
    /// </summary>
    private readonly string _databaseName = $"TestDatabase_{Guid.NewGuid()}";

    public CustomWebApplicationFactory()
    {
        // EN: Reset Serilog static logger before host creation to prevent "logger is already frozen".
        //     Program.cs calls CreateBootstrapLogger() which creates a ReloadableLogger.
        //     UseSerilog() then freezes it. On subsequent factory instances in the same
        //     test process, Log.Logger is already frozen. Reset it to a fresh bootstrap logger.
        // VI: Reset Serilog static logger trước khi tạo host để ngăn "logger is already frozen".
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Warning()
            .WriteTo.Console()
            .CreateBootstrapLogger();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing");

        builder.ConfigureServices(services =>
        {
            // ============================================================
            // EN: Replace DbContext with InMemory database.
            //     Must remove ALL EF Core / Npgsql service registrations
            //     to avoid "multiple database providers" error.
            // VI: Thay thế DbContext bằng InMemory database.
            //     Phải xóa TẤT CẢ đăng ký EF Core / Npgsql services
            //     để tránh lỗi "multiple database providers".
            // ============================================================
            var descriptorsToRemove = services
                .Where(d =>
                    d.ServiceType == typeof(DbContextOptions<OrderContext>) ||
                    d.ServiceType == typeof(OrderContext) ||
                    d.ServiceType.FullName?.Contains("Npgsql") == true ||
                    d.ServiceType.FullName?.Contains("RelationalConnection") == true ||
                    (d.ServiceType.IsGenericType &&
                     d.ServiceType.GetGenericTypeDefinition().FullName?.Contains("DbContextOptions") == true))
                .ToList();

            foreach (var descriptor in descriptorsToRemove)
            {
                services.Remove(descriptor);
            }

            // EN: Also remove the generic DbContextOptions registration
            // VI: Cũng xóa đăng ký DbContextOptions generic
            services.RemoveAll<DbContextOptions<OrderContext>>();

            services.AddDbContext<OrderContext>(options =>
            {
                options.UseInMemoryDatabase(_databaseName);
            });

            // ============================================================
            // EN: Replace IDbConnection (Dapper) with a no-op to avoid PostgreSQL dependency.
            //     Queries that use Dapper with raw SQL won't work with InMemory,
            //     so we provide a mock connection that returns empty results.
            // VI: Thay thế IDbConnection (Dapper) để tránh phụ thuộc PostgreSQL.
            //     Queries dùng Dapper với raw SQL không hoạt động với InMemory,
            //     nên cung cấp mock connection trả về kết quả rỗng.
            // ============================================================
            services.RemoveAll<IDbConnection>();
            services.AddTransient<IDbConnection>(_ => new MockDbConnection());

            // ============================================================
            // EN: Mock external services
            // VI: Mock external services
            // ============================================================

            // EN: Mock IWalletServiceClient — return success for all payment requests
            // VI: Mock IWalletServiceClient — trả về thành công cho tất cả yêu cầu thanh toán
            services.RemoveAll<IWalletServiceClient>();
            services.AddSingleton<IWalletServiceClient, MockWalletServiceClient>();

            // EN: Mock IPosNotificationService — no-op (skip SignalR)
            // VI: Mock IPosNotificationService — no-op (bỏ qua SignalR)
            services.RemoveAll<IPosNotificationService>();
            services.AddSingleton<IPosNotificationService, MockPosNotificationService>();

            // EN: Mock ILineItemStrategy — always return valid for all product types
            // VI: Mock ILineItemStrategy — luôn trả về hợp lệ cho tất cả loại sản phẩm
            services.RemoveAll<ILineItemStrategy>();
            services.AddTransient<ILineItemStrategy>(_ => new MockLineItemStrategy("Physical"));
            services.AddTransient<ILineItemStrategy>(_ => new MockLineItemStrategy("Service"));
            services.AddTransient<ILineItemStrategy>(_ => new MockLineItemStrategy("PreparedFood"));

            // EN: Mock IOrderTenantProvider — bypass tenant filtering in tests
            // VI: Mock IOrderTenantProvider — bỏ qua tenant filtering trong tests
            services.RemoveAll<IOrderTenantProvider>();
            services.AddSingleton<IOrderTenantProvider, MockTenantProvider>();

            services.RemoveAll<ITenantProvider>();
            services.AddSingleton<ITenantProvider, MockApiTenantProvider>();

            // EN: Remove TransactionBehavior — InMemory database does not support transactions.
            //     The ExecutionStrategy from InMemory throws NotSupportedException.
            // VI: Xóa TransactionBehavior — InMemory database không hỗ trợ transactions.
            var transactionBehaviors = services
                .Where(d => d.ServiceType.IsGenericType &&
                             d.ServiceType.GetGenericTypeDefinition() == typeof(MediatR.IPipelineBehavior<,>) &&
                             d.ImplementationType?.Name?.Contains("TransactionBehavior") == true)
                .ToList();
            foreach (var descriptor in transactionBehaviors)
            {
                services.Remove(descriptor);
            }

            // EN: Note: IUserIdProvider is registered by SignalR and needed at runtime.
            //     ClaimsUserIdProvider is already registered in Program.cs, so no action needed.
            // VI: IUserIdProvider đã được đăng ký bởi SignalR và cần tại runtime.
            //     ClaimsUserIdProvider đã đăng ký trong Program.cs, không cần thao tác.

            // ============================================================
            // EN: Replace authentication with test scheme
            // VI: Thay thế authentication bằng test scheme
            // ============================================================
            services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
                    options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
                })
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    TestAuthHandler.SchemeName, _ => { });

        });
    }

    /// <summary>
    /// EN: Seed database after host is created but before tests run.
    /// VI: Seed database sau khi host được tạo nhưng trước khi tests chạy.
    /// </summary>
    protected override IHost CreateHost(IHostBuilder builder)
    {
        var host = base.CreateHost(builder);

        // EN: Seed the InMemory database with OrderStatus enum data
        // VI: Seed InMemory database với OrderStatus enum data
        using var scope = host.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<OrderContext>();
        db.Database.EnsureCreated();

        return host;
    }
}

// ============================================================
// EN: Mock implementations
// VI: Mock implementations
// ============================================================

/// <summary>
/// EN: Mock wallet service client that always succeeds.
/// VI: Mock wallet service client luôn thành công.
/// </summary>
internal class MockWalletServiceClient : IWalletServiceClient
{
    public Task<CreatePaymentResponse?> CreatePaymentAsync(
        Guid orderId, decimal amount, string gateway,
        string returnUrl, string ipAddress,
        CancellationToken cancellationToken = default)
    {
        return Task.FromResult<CreatePaymentResponse?>(
            new CreatePaymentResponse(
                TransactionId: $"MOCK-{Guid.NewGuid():N}",
                PaymentUrl: $"https://mock-gateway.test/pay?txn=MOCK-{orderId}",
                Status: "Pending"));
    }
}

/// <summary>
/// EN: Mock POS notification service — no-op.
/// VI: Mock POS notification service — no-op.
/// </summary>
internal class MockPosNotificationService : IPosNotificationService
{
    public Task NotifyOrderCreatedAsync(Guid shopId, OrderNotificationDto order, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
    public Task NotifyOrderUpdatedAsync(Guid shopId, OrderNotificationDto order, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
    public Task NotifyOrderStatusChangedAsync(Guid shopId, Guid orderId, string oldStatus, string newStatus, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
    public Task NotifyKitchenTicketCreatedAsync(Guid shopId, KitchenTicketNotificationDto ticket, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
    public Task NotifyKitchenTicketUpdatedAsync(Guid shopId, KitchenTicketNotificationDto ticket, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
    public Task NotifyPaymentCompletedAsync(Guid shopId, PaymentNotificationDto payment, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
    public Task NotifyTableStatusChangedAsync(Guid shopId, TableStatusNotificationDto tableStatus, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
}

/// <summary>
/// EN: Mock line item strategy — always validates and executes successfully.
/// VI: Mock line item strategy — luôn validate và execute thành công.
/// </summary>
internal class MockLineItemStrategy : ILineItemStrategy
{
    public string SupportedType { get; }

    public MockLineItemStrategy(string supportedType)
    {
        SupportedType = supportedType;
    }

    public Task<bool> ValidateAsync(OrderItem item, Guid shopId, CancellationToken cancellationToken = default)
        => Task.FromResult(true);

    public Task ExecuteAsync(OrderItem item, Guid shopId, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
}

/// <summary>
/// EN: Mock tenant provider for Infrastructure layer — bypasses tenant filtering.
/// VI: Mock tenant provider cho lớp Infrastructure — bỏ qua tenant filtering.
/// </summary>
internal class MockTenantProvider : IOrderTenantProvider
{
    public Guid? GetCurrentShopId() => null;
    public bool ShouldBypassTenantFilter() => true;
}

/// <summary>
/// EN: Mock API tenant provider — returns test tenant IDs.
/// VI: Mock API tenant provider — trả về test tenant IDs.
/// </summary>
internal class MockApiTenantProvider : ITenantProvider
{
    public Guid? GetCurrentUserId() => TestAuthHandler.TestUserId;
    public Guid? GetCurrentMerchantId() => TestAuthHandler.TestMerchantId;
    public Guid? GetCurrentShopId() => TestAuthHandler.TestShopId;
    public bool IsServiceCall() => false;
    public bool IsAdmin() => true;
}

#pragma warning disable CS8767 // Nullability of reference types in interface implementations (mock objects)

/// <summary>
/// EN: Mock IDbConnection for Dapper queries — returns empty results.
///     Since Dapper queries use raw SQL with PostgreSQL-specific joins,
///     they cannot work with InMemory database. This mock prevents
///     connection errors while allowing command-side tests to pass.
/// VI: Mock IDbConnection cho Dapper queries — trả về kết quả rỗng.
///     Vì Dapper queries dùng raw SQL với PostgreSQL-specific joins,
///     chúng không hoạt động với InMemory database. Mock này ngăn
///     connection errors trong khi cho phép command-side tests pass.
/// </summary>
internal class MockDbConnection : IDbConnection
{
    public string ConnectionString { get; set; } = string.Empty;
    public int ConnectionTimeout => 30;
    public string Database => "test";
    public ConnectionState State => ConnectionState.Open;

    public IDbTransaction BeginTransaction() => new MockDbTransaction(this);
    public IDbTransaction BeginTransaction(IsolationLevel il) => new MockDbTransaction(this);
    public void ChangeDatabase(string databaseName) { }
    public void Close() { }
    public IDbCommand CreateCommand() => new MockDbCommand();
    public void Dispose() { }
    public void Open() { }
}

internal class MockDbTransaction : IDbTransaction
{
    public IDbConnection Connection { get; }
    public IsolationLevel IsolationLevel => IsolationLevel.ReadCommitted;

    public MockDbTransaction(IDbConnection connection)
    {
        Connection = connection;
    }

    public void Commit() { }
    public void Rollback() { }
    public void Dispose() { }
}

internal class MockDbCommand : IDbCommand
{
    public string CommandText { get; set; } = string.Empty;
    public int CommandTimeout { get; set; } = 30;
    public CommandType CommandType { get; set; } = CommandType.Text;
    public IDbConnection? Connection { get; set; }
    public IDataParameterCollection Parameters => new MockParameterCollection();
    public IDbTransaction? Transaction { get; set; }
    public UpdateRowSource UpdatedRowSource { get; set; } = UpdateRowSource.None;

    public void Cancel() { }
    public IDbDataParameter CreateParameter() => new MockDbDataParameter();
    public void Dispose() { }
    public int ExecuteNonQuery() => 0;
    public IDataReader ExecuteReader() => new MockDataReader();
    public IDataReader ExecuteReader(CommandBehavior behavior) => new MockDataReader();
    public object? ExecuteScalar() => 0;
    public void Prepare() { }
}

internal class MockDbDataParameter : IDbDataParameter
{
    public DbType DbType { get; set; }
    public ParameterDirection Direction { get; set; }
    public bool IsNullable => true;
    public string ParameterName { get; set; } = string.Empty;
    public string SourceColumn { get; set; } = string.Empty;
    public DataRowVersion SourceVersion { get; set; }
    public object? Value { get; set; }
    public byte Precision { get; set; }
    public byte Scale { get; set; }
    public int Size { get; set; }
}

internal class MockParameterCollection : IDataParameterCollection
{
    private readonly List<object> _list = new();
    public object this[string parameterName] { get => null!; set { } }
    public object this[int index] { get => _list[index]; set => _list[index] = value; }
    public bool IsFixedSize => false;
    public bool IsReadOnly => false;
    public bool IsSynchronized => false;
    public int Count => _list.Count;
    public object SyncRoot => _list;

    public int Add(object value) { _list.Add(value); return _list.Count - 1; }
    public void Clear() => _list.Clear();
    public bool Contains(string parameterName) => false;
    public bool Contains(object value) => _list.Contains(value);
    public void CopyTo(Array array, int index) { }
    public System.Collections.IEnumerator GetEnumerator() => _list.GetEnumerator();
    public int IndexOf(string parameterName) => -1;
    public int IndexOf(object value) => _list.IndexOf(value);
    public void Insert(int index, object value) => _list.Insert(index, value);
    public void Remove(object value) => _list.Remove(value);
    public void RemoveAt(string parameterName) { }
    public void RemoveAt(int index) => _list.RemoveAt(index);
}

internal class MockDataReader : IDataReader
{
    public int Depth => 0;
    public bool IsClosed => false;
    public int RecordsAffected => 0;
    public int FieldCount => 0;

    public object this[string name] => throw new IndexOutOfRangeException();
    public object this[int i] => throw new IndexOutOfRangeException();

    public void Close() { }
    public void Dispose() { }
    public bool GetBoolean(int i) => false;
    public byte GetByte(int i) => 0;
    public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => 0;
    public char GetChar(int i) => '\0';
    public long GetChars(int i, long fieldOffset, char[]? buffer, int bufferoffset, int length) => 0;
    public IDataReader GetData(int i) => this;
    public string GetDataTypeName(int i) => string.Empty;
    public DateTime GetDateTime(int i) => DateTime.MinValue;
    public decimal GetDecimal(int i) => 0;
    public double GetDouble(int i) => 0;
    public Type GetFieldType(int i) => typeof(object);
    public float GetFloat(int i) => 0;
    public Guid GetGuid(int i) => Guid.Empty;
    public short GetInt16(int i) => 0;
    public int GetInt32(int i) => 0;
    public long GetInt64(int i) => 0;
    public string GetName(int i) => string.Empty;
    public int GetOrdinal(string name) => -1;
    public DataTable GetSchemaTable() => new DataTable();
    public string GetString(int i) => string.Empty;
    public object GetValue(int i) => DBNull.Value;
    public int GetValues(object[] values) => 0;
    public bool IsDBNull(int i) => true;
    public bool NextResult() => false;
    public bool Read() => false;
}
