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();
}