using System.Collections;
using System.Data;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;

internal sealed class ConnectionPlan
{
    public Func<CancellationToken, Task> OpenAsync { get; init; } = _ => Task.CompletedTask;

    public Func<CancellationToken, Task<DbDataReader>> ExecuteReaderAsync { get; init; } =
        _ => Task.FromResult<DbDataReader>(new DataTable().CreateDataReader());

    public bool Disposed { get; set; }

    public int? ObservedCommandTimeoutSeconds { get; set; }

    public string? ObservedCommandText { get; set; }

    public IReadOnlyList<ObservedDbParameter> ObservedParameters { get; set; } = [];

    public CancellationToken ObservedExecuteCancellationToken { get; set; }

    public int SynchronousOpenCount { get; set; }

    public int SynchronousExecuteCount { get; set; }

    public static ConnectionPlan Success(DataTable data) => new()
    {
        ExecuteReaderAsync = _ => Task.FromResult<DbDataReader>(data.CreateDataReader())
    };
}

internal sealed record ObservedDbParameter(
    string Name,
    object? Value,
    DbType? ExplicitDbType,
    ParameterDirection Direction);

internal sealed class FakeConnectionFactory : IDatabaseConnectionFactory
{
    private readonly Queue<ConnectionPlan> _plans;

    public FakeConnectionFactory(params ConnectionPlan[] plans)
    {
        _plans = new Queue<ConnectionPlan>(plans);
    }

    public int CreateCount { get; private set; }

    public List<DbConnection> CreatedConnections { get; } = [];

    public DbConnection Create(DataSourceKind source)
    {
        CreateCount++;
        if (!_plans.TryDequeue(out var plan))
        {
            throw new InvalidOperationException("No fake connection plan remains.");
        }

        var connection = new FakeDbConnection(plan);
        CreatedConnections.Add(connection);
        return connection;
    }

    public int GetCommandTimeoutSeconds(DataSourceKind source) => 5;
}

internal sealed class StubErrorDetector(bool transient) : ITransientDatabaseErrorDetector
{
    public bool IsTransient(DataSourceKind source, Exception exception) => transient;

    public bool IsTimeout(DataSourceKind source, Exception exception) => exception is TimeoutException;

    public int? GetProviderErrorCode(DataSourceKind source, Exception exception) => null;
}

internal sealed class TestDatabaseException(string message) : Exception(message);

internal sealed class FakeDbConnection(ConnectionPlan plan) : DbConnection
{
    private ConnectionState _state = ConnectionState.Closed;

    [AllowNull]
    public override string ConnectionString { get; set; } = string.Empty;

    public override string Database => "FakeDatabase";

    public override string DataSource => "FakeDataSource";

    public override string ServerVersion => "1.0";

    public override ConnectionState State => _state;

    public override void ChangeDatabase(string databaseName)
    {
    }

    public override void Close() => _state = ConnectionState.Closed;

    public override void Open()
    {
        plan.SynchronousOpenCount++;
        throw new InvalidOperationException("The synchronous open path must not be used.");
    }

    public override async Task OpenAsync(CancellationToken cancellationToken)
    {
        await plan.OpenAsync(cancellationToken);
        _state = ConnectionState.Open;
    }

    protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) =>
        throw new NotSupportedException();

    protected override DbCommand CreateDbCommand() => new FakeDbCommand(this, plan);

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            plan.Disposed = true;
            _state = ConnectionState.Closed;
        }

        base.Dispose(disposing);
    }
}

internal sealed class FakeDbCommand(DbConnection connection, ConnectionPlan plan) : DbCommand
{
    private readonly FakeDbParameterCollection _parameters = new();

    [AllowNull]
    public override string CommandText { get; set; } = string.Empty;

    public override int CommandTimeout { get; set; }

    public override CommandType CommandType { get; set; }

    public override bool DesignTimeVisible { get; set; }

    public override UpdateRowSource UpdatedRowSource { get; set; }

    protected override DbConnection? DbConnection { get; set; } = connection;

    protected override DbParameterCollection DbParameterCollection => _parameters;

    protected override DbTransaction? DbTransaction { get; set; }

    public override void Cancel()
    {
    }

    public override int ExecuteNonQuery() => throw new NotSupportedException();

    public override object? ExecuteScalar() => throw new NotSupportedException();

    public override void Prepare()
    {
    }

    protected override DbParameter CreateDbParameter() => new FakeDbParameter();

    protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) =>
        throw RecordSynchronousExecution();

    protected override Task<DbDataReader> ExecuteDbDataReaderAsync(
        CommandBehavior behavior,
        CancellationToken cancellationToken)
    {
        plan.ObservedCommandTimeoutSeconds = CommandTimeout;
        plan.ObservedCommandText = CommandText;
        plan.ObservedParameters = _parameters
            .Cast<DbParameter>()
            .Select(parameter => new ObservedDbParameter(
                parameter.ParameterName,
                parameter.Value,
                parameter is FakeDbParameter fakeParameter && fakeParameter.WasDbTypeSet
                    ? parameter.DbType
                    : null,
                parameter.Direction))
            .ToArray();
        plan.ObservedExecuteCancellationToken = cancellationToken;
        return plan.ExecuteReaderAsync(cancellationToken);
    }

    private Exception RecordSynchronousExecution()
    {
        plan.SynchronousExecuteCount++;
        return new InvalidOperationException("The synchronous command path must not be used.");
    }
}

internal sealed class FakeDbParameter : DbParameter
{
    private DbType _dbType;

    public bool WasDbTypeSet { get; private set; }

    public override DbType DbType
    {
        get => _dbType;
        set
        {
            _dbType = value;
            WasDbTypeSet = true;
        }
    }

    public override ParameterDirection Direction { get; set; }

    public override bool IsNullable { get; set; }

    [AllowNull]
    public override string ParameterName { get; set; } = string.Empty;

    public override int Size { get; set; }

    [AllowNull]
    public override string SourceColumn { get; set; } = string.Empty;

    public override bool SourceColumnNullMapping { get; set; }

    public override object? Value { get; set; }

    public override void ResetDbType()
    {
        _dbType = default;
        WasDbTypeSet = false;
    }
}

internal sealed class FakeDbParameterCollection : DbParameterCollection
{
    private readonly List<DbParameter> _parameters = [];

    public override int Count => _parameters.Count;

    public override object SyncRoot => ((ICollection)_parameters).SyncRoot;

    public override int Add(object value)
    {
        _parameters.Add(RequireParameter(value));
        return _parameters.Count - 1;
    }

    public override void AddRange(Array values)
    {
        foreach (var value in values)
        {
            Add(value ?? throw new ArgumentNullException(nameof(values)));
        }
    }

    public override void Clear() => _parameters.Clear();

    public override bool Contains(object value) =>
        value is DbParameter parameter && _parameters.Contains(parameter);

    public override bool Contains(string value) => IndexOf(value) >= 0;

    public override void CopyTo(Array array, int index) =>
        ((ICollection)_parameters).CopyTo(array, index);

    public override IEnumerator GetEnumerator() => _parameters.GetEnumerator();

    public override int IndexOf(object value) =>
        value is DbParameter parameter ? _parameters.IndexOf(parameter) : -1;

    public override int IndexOf(string parameterName) =>
        _parameters.FindIndex(
            parameter => string.Equals(
                parameter.ParameterName,
                parameterName,
                StringComparison.OrdinalIgnoreCase));

    public override void Insert(int index, object value) =>
        _parameters.Insert(index, RequireParameter(value));

    public override void Remove(object value)
    {
        if (value is DbParameter parameter)
        {
            _parameters.Remove(parameter);
        }
    }

    public override void RemoveAt(int index) => _parameters.RemoveAt(index);

    public override void RemoveAt(string parameterName)
    {
        var index = IndexOf(parameterName);
        if (index >= 0)
        {
            RemoveAt(index);
        }
    }

    protected override DbParameter GetParameter(int index) => _parameters[index];

    protected override DbParameter GetParameter(string parameterName)
    {
        var index = IndexOf(parameterName);
        return index >= 0
            ? _parameters[index]
            : throw new IndexOutOfRangeException();
    }

    protected override void SetParameter(int index, DbParameter value) => _parameters[index] = value;

    protected override void SetParameter(string parameterName, DbParameter value)
    {
        var index = IndexOf(parameterName);
        if (index >= 0)
        {
            _parameters[index] = value;
        }
        else
        {
            _parameters.Add(value);
        }
    }

    private static DbParameter RequireParameter(object value) =>
        value as DbParameter
        ?? throw new ArgumentException("A DbParameter is required.", nameof(value));
}
