using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using RobotNet10.NavigationTune.Data; using RobotNet10.NavigationTune.Hubs; using RobotNet10.NavigationTune.Services; using RobotNet10.NavigationTune.Shared.Interfaces; using RobotNet10.NavigationTune.Shared.Models; namespace RobotNet10.NavigationTune.Controllers; /// /// REST API controller for test execution and control /// [ApiController] [Route("api/[controller]")] public class TuningController( ITuningOrchestrator orchestrator, IParameterManager parameterManager, IScenarioRepository scenarioRepository, ILogger logger) : ControllerBase { /// /// Execute a single test /// [HttpPost("execute")] public async Task> ExecuteTest( [FromBody] ExecuteTestRequest request, CancellationToken cancellationToken = default) { try { // Load scenario and parameter set var scenario = await scenarioRepository.GetByIdAsync(request.ScenarioId); if (scenario == null) return NotFound(new { error = $"Scenario with ID {request.ScenarioId} not found" }); var parameterSet = await parameterManager.GetByIdAsync(request.ParameterSetId); if (parameterSet == null) return NotFound(new { error = $"Parameter set with ID {request.ParameterSetId} not found" }); // Get connection ID from request headers or query var connectionId = Request.Headers["X-Connection-Id"].FirstOrDefault() ?? Request.Query["connectionId"].FirstOrDefault(); // Start test and return immediately so UI can enable Stop/Pause; completion is sent via SignalR (ReceiveTestResult) var result = await orchestrator.StartTestAsync( scenario, parameterSet, connectionId, request.TestRunId, cancellationToken); return Ok(result); } catch (InvalidOperationException ex) { logger.LogWarning(ex, "Invalid operation executing test"); return BadRequest(new { error = ex.Message }); } catch (Exception ex) { logger.LogError(ex, "Error executing test"); return StatusCode(500, new { error = "Failed to execute test", details = ex.Message }); } } /// /// Run batch tests with multiple scenarios /// [HttpPost("batch")] public async Task> RunBatchTests( [FromBody] BatchTestRequest request, CancellationToken cancellationToken = default) { try { // Load parameter set var parameterSet = await parameterManager.GetByIdAsync(request.ParameterSetId); if (parameterSet == null) return NotFound(new { error = $"Parameter set with ID {request.ParameterSetId} not found" }); // Load scenarios var scenarios = new List(); foreach (var scenarioId in request.ScenarioIds) { var scenario = await scenarioRepository.GetByIdAsync(scenarioId); if (scenario == null) { logger.LogWarning("Scenario {ScenarioId} not found, skipping", scenarioId); continue; } scenarios.Add(scenario); } if (scenarios.Count == 0) return BadRequest(new { error = "No valid scenarios found" }); // Execute batch var result = await orchestrator.RunBatchTestsAsync(scenarios, parameterSet, cancellationToken); return Ok(result); } catch (Exception ex) { logger.LogError(ex, "Error running batch tests"); return StatusCode(500, new { error = "Failed to run batch tests", details = ex.Message }); } } /// /// Compare multiple parameter configurations /// [HttpPost("compare")] public async Task> CompareConfigurations( [FromBody] CompareConfigurationsRequest request, CancellationToken cancellationToken = default) { try { // Load scenario var scenario = await scenarioRepository.GetByIdAsync(request.ScenarioId); if (scenario == null) return NotFound(new { error = $"Scenario with ID {request.ScenarioId} not found" }); // Load parameter sets var parameterSets = new List(); foreach (var parameterSetId in request.ParameterSetIds) { var parameterSet = await parameterManager.GetByIdAsync(parameterSetId); if (parameterSet == null) { logger.LogWarning("Parameter set {ParameterSetId} not found, skipping", parameterSetId); continue; } parameterSets.Add(parameterSet); } if (parameterSets.Count == 0) return BadRequest(new { error = "No valid parameter sets found" }); // Compare var result = await orchestrator.CompareConfigurationsAsync(parameterSets, scenario, cancellationToken); return Ok(result); } catch (Exception ex) { logger.LogError(ex, "Error comparing configurations"); return StatusCode(500, new { error = "Failed to compare configurations", details = ex.Message }); } } /// /// Pause running test /// [HttpPost("pause/{testRunId}")] public ActionResult PauseTest(string testRunId) { try { orchestrator.PauseTest(testRunId); return Ok(new { message = "Test paused" }); } catch (Exception ex) { logger.LogError(ex, "Error pausing test {TestRunId}", testRunId); return StatusCode(500, new { error = "Failed to pause test" }); } } /// /// Resume paused test /// [HttpPost("resume/{testRunId}")] public ActionResult ResumeTest(string testRunId) { try { orchestrator.ResumeTest(testRunId); return Ok(new { message = "Test resumed" }); } catch (Exception ex) { logger.LogError(ex, "Error resuming test {TestRunId}", testRunId); return StatusCode(500, new { error = "Failed to resume test" }); } } /// /// Stop running test /// [HttpPost("stop/{testRunId}")] public ActionResult StopTest(string testRunId) { try { orchestrator.StopTest(testRunId); return Ok(new { message = "Test stopped" }); } catch (Exception ex) { logger.LogError(ex, "Error stopping test {TestRunId}", testRunId); return StatusCode(500, new { error = "Failed to stop test" }); } } /// /// Emergency stop /// [HttpPost("emergency-stop/{testRunId}")] public ActionResult EmergencyStop(string testRunId) { try { orchestrator.EmergencyStop(testRunId); return Ok(new { message = "Emergency stop activated" }); } catch (Exception ex) { logger.LogError(ex, "Error emergency stopping test {TestRunId}", testRunId); return StatusCode(500, new { error = "Failed to emergency stop test" }); } } } /// /// Request model for executing a single test. /// When TestRunId is provided, client should call JoinTestSession(TestRunId) before execute so real-time telemetry is received. /// public class ExecuteTestRequest { public Guid ScenarioId { get; set; } public Guid ParameterSetId { get; set; } /// Optional. If set, this Id is used for the test run and for SignalR group test_{TestRunId}. public Guid? TestRunId { get; set; } } /// /// Request model for batch test execution /// public class BatchTestRequest { public List ScenarioIds { get; set; } = new(); public Guid ParameterSetId { get; set; } } /// /// Request model for configuration comparison /// public class CompareConfigurationsRequest { public Guid ScenarioId { get; set; } public List ParameterSetIds { get; set; } = new(); }