using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
using Deal.Telegram.Extensions;
namespace Deal.Telegram.Tests;
// Фейковый клиент Telegram для unit/in-proc тестов (план Task 9: фазовые переходы на fake-клиенте
// абстракции ISessionClient; без сети). Поведение задаётся скриптом: ошибки/результаты кода/пароля,
// QR-вход ждёт явного «сканирования» (CompleteQrScanAsync) — тест управляет моментом авторизации.
internal sealed class FakeSessionClient : ISessionClient
{
///
/// URL, который фейк выдаёт первым колбэком QR-входа.
///
public const string DefaultQrUrl = "tg://login?token=fake_token";
private readonly TaskCompletionSource _qrScanTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
private bool _authorized;
///
/// Создаёт фейк под ключи приложения (как реальный клиент).
///
/// api_id приложения.
/// api_hash приложения.
/// Байты «сохранённой сессии» (для проверки передачи в фабрику).
public FakeSessionClient(
int apiId,
string apiHash,
byte[]? storedSession)
{
ApiId = apiId;
ApiHash = apiHash;
SessionBytes = storedSession ?? SessionMarker;
_authorized = storedSession is not null;
}
///
/// Сколько раз вызван ConnectAsync (проверка попыток переподключения в тестах).
///
public int ConnectAttempts { get; private set; }
///
/// Непустой — ConnectAsync ждёт завершения этой задачи (эмуляция «зависшего» connect;
/// отмена токена прерывает ожидание). null — мгновенный успех.
///
public TaskCompletionSource? ConnectGate { get; set; }
///
/// True — первый URL QR не выдаётся сразу (эмуляция ожидания QR до сканирования/отмены).
///
public bool DelayFirstQrUrl { get; set; }
///
/// True — фоновый QR-вход (StartQrAsync) остановлен отменой до сканирования.
///
public bool QrLoginCancelled { get; private set; }
///
/// Маркерные байты сессии (детект сохранения файла в тестах).
///
public static byte[] SessionMarker { get; } = [1, 3, 3, 7, 42];
///
/// Исключение, которое бросит RequestCodeAsync (null — успех).
///
public Exception? RequestCodeError { get; set; }
///
/// Исключение SubmitCodeAsync (null — успех).
///
public Exception? CodeError { get; set; }
///
/// Исключение SubmitPasswordAsync (null — успех).
///
public Exception? PasswordError { get; set; }
///
/// Результат SubmitCodeAsync: "password" — нужен 2FA; null — авторизация завершена.
///
public string? CodeResult { get; set; }
///
/// Аккаунт, возвращаемый GetAccountAsync.
///
public string Account { get; set; } = "@fake_user";
///
/// Был ли вызван LogOutAsync.
///
public bool LoggedOut { get; private set; }
///
/// Запрошенные номера (RequestCodeAsync).
///
public List RequestedPhones { get; } = [];
///
/// Отправленные коды (SubmitCodeAsync).
///
public List SubmittedCodes { get; } = [];
///
/// Отправленные пароли (SubmitPasswordAsync).
///
public List SubmittedPasswords { get; } = [];
///
/// Был ли вызван StartQrAsync.
///
public bool QrStarted { get; private set; }
///
/// Завершает «сканирование» QR — авторизует фейк (как подтверждение на телефоне).
///
public void CompleteQrScan()
=> _qrScanTcs.TrySetResult(true);
///
public bool IsAuthorized => _authorized;
///
public bool IsConnected { get; private set; }
///
public int ApiId { get; }
///
public string ApiHash { get; }
///
public byte[] SessionBytes { get; private set; }
///
public async Task ConnectAsync(CancellationToken cancellationToken)
{
ConnectAttempts++;
if (ConnectGate is not null)
{
// «Зависший» connect: ждём gate (или отмену токена — реальный клиент прерывается по ct).
await ConnectGate.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
}
IsConnected = true;
}
///
public Task RequestCodeAsync(string phone, CancellationToken cancellationToken)
{
RequestedPhones.Add(phone);
if (RequestCodeError is not null)
{
throw RequestCodeError;
}
return Task.CompletedTask;
}
///
public Task SubmitCodeAsync(string code, CancellationToken cancellationToken)
{
SubmittedCodes.Add(code);
if (CodeError is not null)
{
throw CodeError;
}
if (CodeResult == "password")
{
return Task.FromResult(CodeResult);
}
_authorized = true;
SessionBytes = SessionMarker;
return Task.FromResult(null);
}
///
public Task SubmitPasswordAsync(string password, CancellationToken cancellationToken)
{
SubmittedPasswords.Add(password);
if (PasswordError is not null)
{
throw PasswordError;
}
_authorized = true;
SessionBytes = SessionMarker;
return Task.CompletedTask;
}
///
public async Task StartQrAsync(Action onQrUrl, CancellationToken cancellationToken)
{
QrStarted = true;
// Реальный WTelegramClient при QR-входе сам устанавливает соединение (LoginWithQRCode → ConnectAsync).
IsConnected = true;
if (!DelayFirstQrUrl)
{
onQrUrl(DefaultQrUrl);
}
try
{
await _qrScanTcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Отмена до сканирования: фоновый вход остановлен (не «скрытая» авторизация).
QrLoginCancelled = true;
throw;
}
_authorized = true;
SessionBytes = SessionMarker;
}
///
public Task LogOutAsync(CancellationToken cancellationToken)
{
LoggedOut = true;
_authorized = false;
return Task.CompletedTask;
}
///
public Task GetAccountAsync(CancellationToken cancellationToken)
=> Task.FromResult(Account);
///
public event Func? MessageReceived;
///
/// Список диалогов, который фейк возвращает из GetDialogsAsync (по умолчанию пуст).
///
public List Dialogs { get; } = [];
///
/// Сообщения диалогов (ключ — подписанный id), которые возвращает GetMessagesAsync.
///
public Dictionary> Messages { get; } = new(StringComparer.Ordinal);
///
/// Сколько раз вызван GetDialogsAsync.
///
public int DialogListCallCount { get; private set; }
///
/// Диалоги, которые GetMessagesAsync/MarkReadAsync получали (в порядке вызовов).
///
public List ReadDialogs { get; } = [];
///
/// Диалоги, помеченные прочитанными (MarkReadAsync), в порядке вызовов.
///
public List MarkedReadDialogs { get; } = [];
///
/// Исключение GetDialogsAsync (null — успех).
///
public Exception? DialogsError { get; set; }
///
/// Исключение GetMessagesAsync (null — успех).
///
public Exception? MessagesError { get; set; }
///
public Task> GetDialogsAsync(int limit, CancellationToken cancellationToken)
{
DialogListCallCount++;
if (DialogsError is not null)
{
throw DialogsError;
}
return Task.FromResult>(Dialogs.ToArray());
}
///
public Task> GetMessagesAsync(
string dialogId,
int limit,
CancellationToken cancellationToken)
{
ReadDialogs.Add(dialogId);
if (MessagesError is not null)
{
throw MessagesError;
}
return Task.FromResult>(
Messages.TryGetValue(dialogId, out List? messages)
? messages.Take(limit).ToArray()
: []);
}
///
public Task MarkReadAsync(string dialogId, CancellationToken cancellationToken)
{
MarkedReadDialogs.Add(dialogId);
return Task.CompletedTask;
}
// --- Discovery (план Task 11): данные/ошибки операций задаёт тест, сети нет ---
///
/// Результат поиска, который фейк возвращает из SearchAsync (chats затем users).
///
public List SearchResults { get; } = [];
///
/// Поисковые запросы (query/limit), в порядке вызовов.
///
public List<(string Query, int Limit)> SearchQueries { get; } = [];
///
/// Исключение SearchAsync (null — успех).
///
public Exception? SearchError { get; set; }
///
/// Инфо источников (ключ — подписанный id), возвращаемое GetInfoAsync.
///
public Dictionary SourceInfos { get; } = new(StringComparer.Ordinal);
///
/// Id источников, по которым запрошено инфо, в порядке вызовов.
///
public List InfoRequests { get; } = [];
///
/// Результаты чтения выборки (ключ — подписанный id), возвращаемые ReadForEvalAsync.
///
public Dictionary ReadForEvalResults { get; } = new(StringComparer.Ordinal);
///
/// Id источников, по которым запрошено чтение, в порядке вызовов.
///
public List ReadRequests { get; } = [];
///
/// Username вступлений (после нормализации), в порядке вызовов.
///
public List JoinedUsernames { get; } = [];
///
/// Исключение JoinAsync (null — успех; flood эмулируется SessionException RESOURCE_EXHAUSTED).
///
public Exception? JoinError { get; set; }
///
/// Id диалогов выхода, в порядке вызовов.
///
public List LeaveCalls { get; } = [];
///
/// Исключение LeaveAsync (null — успех).
///
public Exception? LeaveError { get; set; }
///
public Task> SearchAsync(
string query,
int limit,
CancellationToken cancellationToken)
{
SearchQueries.Add((query, limit));
if (SearchError is not null)
{
throw SearchError;
}
return Task.FromResult>(SearchResults.ToArray());
}
///
public Task GetInfoAsync(string dialogId, CancellationToken cancellationToken)
{
InfoRequests.Add(dialogId);
return Task.FromResult(
SourceInfos.TryGetValue(dialogId, out TelegramSourceInfo? info)
? info
: new TelegramSourceInfo(dialogId, dialogId, string.Empty, string.Empty, participants: null, isForum: false));
}
///
public Task ReadForEvalAsync(
string dialogId,
int limit,
CancellationToken cancellationToken)
{
ReadRequests.Add(dialogId);
return Task.FromResult(
ReadForEvalResults.TryGetValue(dialogId, out DiscoveryReadResult? result)
? result
: DiscoveryReadResult.Empty);
}
///
public Task JoinAsync(string username, CancellationToken cancellationToken)
{
JoinedUsernames.Add(username);
if (JoinError is not null)
{
throw JoinError;
}
return Task.CompletedTask;
}
///
public Task LeaveAsync(string dialogId, CancellationToken cancellationToken)
{
LeaveCalls.Add(dialogId);
if (LeaveError is not null)
{
throw LeaveError;
}
return Task.CompletedTask;
}
///
/// Поднимает событие нового сообщения (эмуляция realtime-события Telegram).
///
/// Входящее сообщение диалога.
public async Task RaiseMessageAsync(TelegramMessage message)
{
Func? handler = MessageReceived;
if (handler is null)
{
return;
}
foreach (Delegate subscriber in handler.GetInvocationList())
{
await ((Func)subscriber)(message).ConfigureAwait(false);
}
}
///
public ValueTask DisposeAsync()
{
IsConnected = false;
return ValueTask.CompletedTask;
}
}
// Фейковая фабрика клиентов: создаёт FakeSessionClient и запоминает экземпляры
// (тест управляет «сканированием» QR и проверяет передачи ключей/байт сессии).
internal sealed class FakeClientFactory : ITelegramClientFactory
{
///
/// Все созданные фабрикой клиенты (в порядке создания).
///
public List CreatedClients { get; } = [];
///
/// Колбэк настройки клиента перед возвратом (null — клиент по умолчанию).
///
public Action? OnClientCreated { get; set; }
///
public ISessionClient Create(
int apiId,
string apiHash,
byte[]? storedSession)
{
var client = new FakeSessionClient(apiId, apiHash, storedSession);
OnClientCreated?.Invoke(client);
CreatedClients.Add(client);
return client;
}
}