using GisConverter.Lib.Converters;
using GisConverter.Lib.Factories;
using GisConverter.Lib.Licensing;
using GisConverter.Lib.Logging;
using GisConverter.Lib.Models;
namespace GisConverter.ConsoleApp
{
///
/// Entry point and CLI dispatch for the GisConverter console application.
///
///
/// Responsibilities
/// - Exposes the single top-level command gis_convert which routes an input GIS artifact to a target format converter.
/// - Performs host-level concerns only: argument parsing, basic validation, logging initialization, license application
/// and converter selection. Conversion work is delegated to and .
///
/// Design notes
/// - The class keeps behavior deterministic and minimal because tests invoke
/// directly and assert on return codes and console output.
/// - Exit codes (see ) are the stable contract with callers and automation.
/// - Keep user-facing messages concise and stable; tests and automation rely on predictable tokens and exit codes.
///
public class Program
{
// Exit codes for the CLI
private enum ExitCode : int
{
Success = 0, // Successful execution
AppError = 1, // invalid args, wrong command
ConversionFailed = 2, // converter returned Failure or null
Unexpected = 3, // unexpected exception
}
private const int MIN_REQUIRED_ARGS = 5;
private const int LOG_ARGS_START_INDEX = 5;
///
/// Process entry point. Sets up basic host concerns and delegates to .
///
/// Command-line arguments supplied by the caller.
///
/// - Enables UTF-8 console output so international characters and emoji render correctly.
/// - In DEBUG builds uses to supply developer scenarios via
/// the GISCONVERTER_DEBUG_TARGET environment variable.
/// - Calls and returns its numeric exit code; this method is intentionally minimal
/// and non-throwing so test harnesses and hosts can rely on the returned value.
///
private static int Main(string[] args)
{
// Enable Unicode (UTF-8) output for emoji and international characters
Console.OutputEncoding = System.Text.Encoding.UTF8;
args = EnsureDebugArgs(args);
return Run(args);
}
///
/// In DEBUG builds optionally replaces supplied args with a developer scenario selected via environment variables.
///
/// Original command-line arguments (may be null or incomplete).
///
/// - In DEBUG builds: either the original args (if sufficient), a selected debug scenario args array, or an empty
/// args array when no scenario is selected (which causes the app to print available scenarios).
/// - In RELEASE builds: returns the original args or an empty array if null.
///
///
/// Purpose
/// - Provide an ergonomic mechanism for developers to run repeatable end-to-end scenarios without editing code.
///
/// Behavior
/// - When running under DEBUG and no sufficient args are provided the method probes the environment variable
/// __GISCONVERTER_DEBUG_TARGET__ and returns the matching prebuilt scenario from .
/// - Supports a special value of "all" which will execute every scenario in sequence and then exit the process.
///
/// Test guidance
/// - Tests should not rely on this behavior. Unit and integration tests invoke directly
/// with explicit args. This helper is strictly aimed at developer convenience during interactive debugging.
///
private static string[] EnsureDebugArgs(string[] args)
{
#if DEBUG
if (args != null && args.Length >= MIN_REQUIRED_ARGS)
return args;
var baseDir = Environment.GetEnvironmentVariable("GISCONVERTER_TEST_BASE")
?? @"D:\GisConverter\Tests";
var scenarios = BuildDebugScenarios(baseDir);
var debugTarget = Environment.GetEnvironmentVariable("GISCONVERTER_DEBUG_TARGET")?.Trim().ToLowerInvariant();
// Special case: run all scenarios in sequence
if (debugTarget == "all")
{
Console.WriteLine("Running all debug scenarios:");
Console.WriteLine();
foreach (var kvp in scenarios.OrderBy(s => s.Key))
{
Console.WriteLine($"Executing scenario: {kvp.Key}");
Console.WriteLine(new string('-', 60));
var exitCode = Run(kvp.Value);
Console.WriteLine($"Result: {(ExitCode)exitCode}");
Console.WriteLine();
}
Environment.Exit(0);
}
// Try to return the requested single scenario
if (!string.IsNullOrWhiteSpace(debugTarget) && scenarios.TryGetValue(debugTarget, out var selectedArgs))
{
Console.WriteLine($"Debug mode: using scenario '{debugTarget}'");
Console.WriteLine();
return selectedArgs;
}
// If no valid scenario was specified, show available options
if (!string.IsNullOrWhiteSpace(debugTarget))
{
Console.WriteLine($"Warning: Unknown scenario: '{debugTarget}'");
}
Console.WriteLine("Available scenarios (set GISCONVERTER_DEBUG_TARGET):");
foreach (var key in scenarios.Keys.OrderBy(k => k))
{
Console.WriteLine($" - {key}");
}
Console.WriteLine(" - all (runs all scenarios)");
Console.WriteLine();
return Array.Empty();
#else
return args ?? Array.Empty();
#endif
}
///
/// Builds a dictionary of debug scenarios to developer-friendly argument arrays.
///
/// Root folder where test scenario subfolders live (contains Input/Output/Temp/Log folders).
/// Dictionary mapping scenario key to a complete args array that can be passed to .
///
/// - Each scenario returns an args array following the CLI contract:
/// [ "gis_convert", inputPath, format, outputFolder, tempFolder, "Log", logFilePath ]
/// - The helper centralizes sample naming and path composition to keep debug scenarios consistent.
/// - This method is used only for developer debugging scenarios and is not relied upon by automated tests.
///
private static Dictionary BuildDebugScenarios(string baseDir)
{
var scenarios = new Dictionary(StringComparer.OrdinalIgnoreCase);
void AddScenario(string scenarioKey, string format, string inputFileName)
{
var formatFolder = format;
scenarios[scenarioKey] = new[]
{
"gis_convert",
Path.Combine(baseDir, formatFolder, "Input", inputFileName),
format,
Path.Combine(baseDir, formatFolder, "Output", scenarioKey),
Path.Combine(baseDir, formatFolder, "Temp", scenarioKey),
"Log",
Path.Combine(baseDir, formatFolder, "Log", $"{scenarioKey.ToLowerInvariant()}_log.txt")
};
}
// Csv test cases - note the different extensions
AddScenario("csv1", "Csv", "features.7z");
AddScenario("csv2", "Csv", "features.csv");
AddScenario("csv3", "Csv", "features.zip");
// Gml test cases
AddScenario("gml1", "Gml", "gml_with_attribute_collection_schema.7z");
AddScenario("gml2", "Gml", "gml_with_attribute_collection_schema.zip");
AddScenario("gml3", "Gml", "gml_without_attribute_collection_schema.7z");
AddScenario("gml4", "Gml", "gml_without_attribute_collection_schema.gml");
AddScenario("gml5", "Gml", "gml_without_attribute_collection_schema.zip");
// Shapefile test cases
AddScenario("shapefile1", "Shapefile", "ShapeFiles.7z");
AddScenario("shapefile2", "Shapefile", "Vector.7z");
AddScenario("shapefile3", "Shapefile", "לא סטטוטורי - גבול מחוז מאוחד.7z");
AddScenario("shapefile4", "Shapefile", "שכבות מידע (Arc View).7z");
return scenarios;
}
///
/// Main dispatcher for command-line operations.
///
/// Command-line arguments passed to the application.
/// Integer exit code describing result (see ).
///
/// - Recognizes top-level help flags and the single supported command gis_convert.
/// - Applies licensing via before performing other work;
/// when licensing fails a friendly error is written to stderr and the method returns an application error.
/// - Catches unexpected exceptions and returns while printing the exception message
/// for diagnostics. The method itself avoids throwing to keep callers (tests, CI) deterministic.
///
public static int Run(string[] args)
{
// Apply license FIRST before doing anything else
if (!AsposeLicenseManager.ApplyLicense())
{
Console.Error.WriteLine("⚠️ Failed to apply aspose license.");
Console.Error.WriteLine("the program cannot continue without a valid license.");
return (int)ExitCode.AppError;
}
if (args.Length == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h")
{
ShowHelp();
return (int)ExitCode.Success;
}
if (args[0] == "--help-formats")
{
ShowHelpFormats();
return (int)ExitCode.Success;
}
// Extract the command from the first argument.
string command = args[0].ToLowerInvariant();
try
{
if (!args[0].Equals("gis_convert", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"❌ Unknown command: {args[0]}\n");
ShowHelp();
return (int)ExitCode.AppError;
}
return RunGisConverter(args);
}
catch (Exception ex)
{
Console.WriteLine("❌ Error:");
Console.WriteLine(ex.Message);
return (int)ExitCode.Unexpected;
}
}
///
/// Executes the gis_convert command and returns an exit code.
///
/// Command-line arguments where:
/// args[1] = input path, args[2] = target format, args[3] = output folder, args[4] = temp folder,
/// optional args starting at index 5 may contain logging options.
/// Exit code representing the command outcome.
///
/// Responsibilities and behavior:
/// - Validates argument count and prints usage when insufficient arguments are supplied.
/// - Parses optional logging arguments via and initializes logging
/// with (best-effort — logging failures do not abort conversion).
/// - Resolves an appropriate converter using and
/// invokes its method.
/// - Maps converter outcomes to stable exit codes:
/// - on success,
/// - when the converter reports failure or returns null,
/// - for validation/usage/detection issues.
/// - Defensive: catches unexpected exceptions from converters and reports them while returning .
///
/// Notes for tests and consumers:
/// - Tests may rely on specific console tokens and exit codes; prefer case-insensitive substring checks when asserting messages.
/// - The method intentionally writes concise, human-readable diagnostics to stdout/stderr to aid developers and CI diagnostics.
/// - When integrating with automation, prefer checking the numeric exit code first and then capture console output for richer diagnostics.
///
private static int RunGisConverter(string[] args)
{
// Require at least: command (gis_convert), gis input file path, gis target format option, output folder path, out temp folder.
if (args.Length < MIN_REQUIRED_ARGS)
{
Console.WriteLine("Usage: gis_convert