diff --git a/MyChat/MyChat.Tests/ArgumentsTests.cs b/MyChat/MyChat.Tests/ArgumentsTests.cs
new file mode 100644
index 0000000..694e544
--- /dev/null
+++ b/MyChat/MyChat.Tests/ArgumentsTests.cs
@@ -0,0 +1,48 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyChat.Test;
+
+using MyChat;
+using NUnit.Framework;
+
+///
+/// Unit tests for parsing logic.
+///
+public class ArgumentsTests
+{
+ ///
+ /// Tests that parsing a single argument returns server mode.
+ ///
+ [Test]
+ public void Arguments_Parse_SingleArgument_ReturnsServerMode()
+ {
+ var args = new[] { "5000" };
+ var parsed = Arguments.Parse(args);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(parsed.IsServer, Is.True);
+ Assert.That(parsed.Port, Is.EqualTo(5000));
+ Assert.That(parsed.IpAddress, Is.Null);
+ });
+ }
+
+ ///
+ /// Tests that parsing two arguments returns client mode.
+ ///
+ [Test]
+ public void Arguments_Parse_TwoArguments_ReturnsClientMode()
+ {
+ var args = new[] { "127.0.0.1", "5000" };
+ var parsed = Arguments.Parse(args);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(parsed.IsServer, Is.False);
+ Assert.That(parsed.Port, Is.EqualTo(5000));
+ Assert.That(parsed.IpAddress, Is.EqualTo("127.0.0.1"));
+ });
+ }
+}
\ No newline at end of file
diff --git a/MyChat/MyChat.Tests/ChatRunnerTests.cs b/MyChat/MyChat.Tests/ChatRunnerTests.cs
new file mode 100644
index 0000000..e293bca
--- /dev/null
+++ b/MyChat/MyChat.Tests/ChatRunnerTests.cs
@@ -0,0 +1,73 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyChat.Test;
+
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using MyChat;
+using NUnit.Framework;
+
+///
+/// Tests for .
+///
+[TestFixture]
+public class ChatRunnerTests
+{
+ ///
+ /// Tests that a server and client can exchange a single message.
+ ///
+ /// A representing the asynchronous unit test.
+ [Test]
+ public async Task ChatRunner_RunAsync_ServerReceive()
+ {
+ const string ipAddress = "127.0.0.1";
+ const int port = 5005;
+
+ string serverReceived = string.Empty;
+ string clientMessage = "Hello!";
+
+ var serverTask = Task.Run(async () =>
+ {
+ var listener = new TcpListener(IPAddress.Parse(ipAddress), port);
+ listener.Start();
+
+ using var client = await listener.AcceptTcpClientAsync();
+ using var stream = client.GetStream();
+ using var reader = new StreamReader(stream);
+ using var writer = new StreamWriter(stream) { AutoFlush = true };
+
+ serverReceived = await reader.ReadLineAsync() ?? string.Empty;
+ await writer.WriteLineAsync(serverReceived);
+
+ listener.Stop();
+ });
+
+ await Task.Delay(200);
+
+ var clientTask = Task.Run(async () =>
+ {
+ using var client = new TcpClient();
+ await client.ConnectAsync(IPAddress.Parse(ipAddress), port);
+ using var stream = client.GetStream();
+ using var reader = new StreamReader(stream);
+ using var writer = new StreamWriter(stream) { AutoFlush = true };
+
+ await writer.WriteLineAsync(clientMessage);
+ var received = await reader.ReadLineAsync()!;
+ return received;
+ });
+
+ var clientReceived = await clientTask;
+ await serverTask;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(serverReceived, Is.EqualTo(clientMessage));
+ Assert.That(clientReceived, Is.EqualTo(clientMessage));
+ });
+ }
+}
diff --git a/MyChat/MyChat.Tests/MyChat.Tests.csproj b/MyChat/MyChat.Tests/MyChat.Tests.csproj
new file mode 100644
index 0000000..34334e1
--- /dev/null
+++ b/MyChat/MyChat.Tests/MyChat.Tests.csproj
@@ -0,0 +1,39 @@
+
+
+
+ net9.0
+ latest
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MyChat/MyChat.Tests/stylecop.json b/MyChat/MyChat.Tests/stylecop.json
new file mode 100644
index 0000000..76c8e76
--- /dev/null
+++ b/MyChat/MyChat.Tests/stylecop.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "documentationRules": {
+ "companyName": "khusainovilas",
+ "copyrightText": "Copyright (c) {companyName}. All rights reserved."
+ }
+ }
+}
\ No newline at end of file
diff --git a/MyChat/MyChat.sln b/MyChat/MyChat.sln
new file mode 100644
index 0000000..0692ba5
--- /dev/null
+++ b/MyChat/MyChat.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyChat", "MyChat\MyChat.csproj", "{DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyChat.Tests", "MyChat.Tests\MyChat.Tests.csproj", "{A801DB41-1484-46AA-9D09-0CFEB18C7271}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/MyChat/MyChat/Arguments.cs b/MyChat/MyChat/Arguments.cs
new file mode 100644
index 0000000..6462cbd
--- /dev/null
+++ b/MyChat/MyChat/Arguments.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyChat;
+
+using System;
+
+///
+/// Represents parsed command-line arguments for the chat application.
+///
+public class Arguments
+{
+ private Arguments(int port, string? ipAddress)
+ {
+ this.Port = port;
+ this.IpAddress = ipAddress;
+ }
+
+ ///
+ /// Gets the network port number.
+ ///
+ public int Port { get; }
+
+ ///
+ /// Gets the IP address for client mode.
+ ///
+ public string? IpAddress { get; }
+
+ ///
+ /// Gets a value indicating whether returns the value, is it a server or an application.
+ ///
+ public bool IsServer => this.IpAddress is null;
+
+ ///
+ /// Parses command-line arguments.
+ ///
+ /// Command-line arguments.
+ /// Parsed instance.
+ public static Arguments Parse(string[] args)
+ {
+ if (args.Length == 1)
+ {
+ return new Arguments(int.Parse(args[0]), null);
+ }
+
+ if (args.Length == 2)
+ {
+ return new Arguments(int.Parse(args[1]), args[0]);
+ }
+
+ throw new ArgumentException("Invalid number of command-line arguments.");
+ }
+}
diff --git a/MyChat/MyChat/ChatRunner.cs b/MyChat/MyChat/ChatRunner.cs
new file mode 100644
index 0000000..a1b6c17
--- /dev/null
+++ b/MyChat/MyChat/ChatRunner.cs
@@ -0,0 +1,133 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyChat;
+
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+///
+/// Provides methods for running a chat application in server or client mode.
+///
+public static class ChatRunner
+{
+ private static readonly List ConnectedClients = [];
+
+ ///
+ /// Starts the chat application based on the provided arguments.
+ ///
+ /// Arguments containing port and optional IP address.
+ /// A representing the async operation.
+ public static async Task RunAsync(Arguments arguments)
+ {
+ if (arguments.IsServer)
+ {
+ await RunServerAsync(arguments.Port);
+ }
+ else
+ {
+ await RunClientAsync(arguments.IpAddress!, arguments.Port);
+ }
+ }
+
+ ///
+ /// Runs the chat application as a server listening.
+ ///
+ /// Port number to listen on.
+ /// Representing the asynchronous server operation.
+ public static async Task RunServerAsync(int port)
+ {
+ var listener = new TcpListener(IPAddress.Any, port);
+ listener.Start();
+ Console.WriteLine($"Server listening on port {port}...");
+
+ while (true)
+ {
+ var client = await listener.AcceptTcpClientAsync();
+ lock (ConnectedClients)
+ {
+ ConnectedClients.Add(client);
+ }
+
+ var endpoint = client.Client.RemoteEndPoint?.ToString() ?? "Unknown";
+
+ Console.WriteLine($"Client connected: {endpoint}");
+
+ _ = Task.Run(async () =>
+ {
+ await RunChatAsync(client, endpoint);
+
+ Console.WriteLine($"Client disconnected: {endpoint}");
+ lock (ConnectedClients)
+ {
+ ConnectedClients.Remove(client);
+ }
+
+ lock (ConnectedClients)
+ {
+ if (ConnectedClients.Count == 0)
+ {
+ Console.WriteLine("No clients connected. Server shutting down.");
+ Environment.Exit(0);
+ }
+ }
+ });
+ }
+ }
+
+ ///
+ /// Runs the chat application as a client.
+ ///
+ /// IP address of the server to connect to.
+ /// Port number of the server.
+ /// Representing the async client operation.
+ public static async Task RunClientAsync(string ip, int port)
+ {
+ using var client = new TcpClient();
+ await client.ConnectAsync(ip, port);
+ Console.WriteLine($"Connected to server {ip}:{port}");
+ await RunChatAsync(client, "Server");
+ }
+
+ private static async Task RunChatAsync(TcpClient client, string name)
+ {
+ using var stream = client.GetStream();
+ using var reader = new StreamReader(stream);
+ using var writer = new StreamWriter(stream) { AutoFlush = true };
+
+ var consoleTask = Task.Run(async () =>
+ {
+ while (true)
+ {
+ var line = Console.ReadLine();
+ if (line == null || line.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ await writer.WriteLineAsync(line);
+ }
+ });
+
+ var networkTask = Task.Run(async () =>
+ {
+ while (true)
+ {
+ var line = await reader.ReadLineAsync();
+ if (line == null)
+ {
+ break;
+ }
+
+ Console.WriteLine($"> {line}");
+ }
+ });
+
+ await Task.WhenAny(consoleTask, networkTask);
+ client.Close();
+ }
+}
diff --git a/MyChat/MyChat/MyChat.csproj b/MyChat/MyChat/MyChat.csproj
new file mode 100644
index 0000000..4f74535
--- /dev/null
+++ b/MyChat/MyChat/MyChat.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/MyChat/MyChat/Program.cs b/MyChat/MyChat/Program.cs
new file mode 100644
index 0000000..de02506
--- /dev/null
+++ b/MyChat/MyChat/Program.cs
@@ -0,0 +1,15 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+using MyChat;
+
+try
+{
+ var arguments = Arguments.Parse(args);
+ await ChatRunner.RunAsync(arguments);
+}
+catch (Exception ex)
+{
+ Console.WriteLine($"Error: {ex.Message}");
+}
diff --git a/MyChat/MyChat/stylecop.json b/MyChat/MyChat/stylecop.json
new file mode 100644
index 0000000..76c8e76
--- /dev/null
+++ b/MyChat/MyChat/stylecop.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "documentationRules": {
+ "companyName": "khusainovilas",
+ "copyrightText": "Copyright (c) {companyName}. All rights reserved."
+ }
+ }
+}
\ No newline at end of file