using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using RobotNet10.NavigationTune.Data;
using RobotNet10.NavigationTune.Shared.Models;
using StraightLineScenario = RobotNet10.NavigationTune.Scenarios.StraightLineScenario;
using CircleScenario = RobotNet10.NavigationTune.Scenarios.CircleScenario;
using CustomPathScenario = RobotNet10.NavigationTune.Scenarios.CustomPathScenario;
namespace RobotNet10.NavigationTune.Controllers;
///
/// DTO for TestScenario API responses
///
public class TestScenarioDto
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public TrajectoryType Type { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsDefault { get; set; }
public string ConfigJson { get; set; } = string.Empty;
public static TestScenarioDto FromTestScenario(TestScenario scenario, TestScenarioEntity entity)
{
return new TestScenarioDto
{
Id = scenario.Id,
Name = scenario.Name,
Description = scenario.Description,
Type = scenario.Type,
CreatedAt = scenario.CreatedAt,
IsDefault = scenario.IsDefault,
ConfigJson = entity.ConfigJson
};
}
}
///
/// Request DTO for Create/Update scenario. Uses Type + ConfigJson to avoid deserializing abstract TestScenario.
///
public class CreateOrUpdateScenarioRequest
{
public Guid? Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public TrajectoryType Type { get; set; }
public bool IsDefault { get; set; }
/// JSON string of the scenario-specific config (serialized StraightLineScenario, CircleScenario, or CustomPathScenario).
public string ConfigJson { get; set; } = string.Empty;
}
///
/// REST API controller for test scenarios management
///
[ApiController]
[Route("api/[controller]")]
public class ScenariosController(
IScenarioRepository scenarioRepository,
ILogger logger) : ControllerBase
{
private readonly IScenarioRepository _scenarioRepository = scenarioRepository;
private readonly ILogger _logger = logger;
///
/// Get all scenarios
///
[HttpGet]
public async Task>> GetAll()
{
try
{
var entities = await _scenarioRepository.GetAllEntitiesAsync();
var dtos = new List();
foreach (var entity in entities)
{
var scenario = entity.ToTestScenario();
dtos.Add(TestScenarioDto.FromTestScenario(scenario, entity));
}
return Ok(dtos);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting all scenarios");
return StatusCode(500, new { error = "Failed to retrieve scenarios" });
}
}
///
/// Get default scenarios
///
[HttpGet("defaults")]
public async Task>> GetDefaults()
{
try
{
var entities = await _scenarioRepository.GetDefaultScenarioEntitiesAsync();
var dtos = new List();
foreach (var entity in entities)
{
var scenario = entity.ToTestScenario();
dtos.Add(TestScenarioDto.FromTestScenario(scenario, entity));
}
return Ok(dtos);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting default scenarios");
return StatusCode(500, new { error = "Failed to retrieve default scenarios" });
}
}
///
/// Get scenario by ID
///
[HttpGet("{id:guid}")]
public async Task> GetById(Guid id)
{
try
{
var entity = await _scenarioRepository.GetEntityByIdAsync(id);
if (entity == null)
return NotFound(new { error = $"Scenario with ID {id} not found" });
var scenario = entity.ToTestScenario();
return Ok(TestScenarioDto.FromTestScenario(scenario, entity));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting scenario {Id}", id);
return StatusCode(500, new { error = "Failed to retrieve scenario" });
}
}
///
/// Create new scenario. Accepts Type + ConfigJson to avoid deserializing abstract TestScenario.
///
[HttpPost]
public async Task> Create([FromBody] CreateOrUpdateScenarioRequest request)
{
try
{
var scenario = DeserializeScenarioFromRequest(request);
if (scenario.Id == Guid.Empty)
scenario.Id = Guid.NewGuid();
var id = await _scenarioRepository.SaveAsync(scenario);
var entity = await _scenarioRepository.GetEntityByIdAsync(id);
if (entity == null)
return StatusCode(500, new { error = "Failed to retrieve created scenario" });
var dto = TestScenarioDto.FromTestScenario(entity.ToTestScenario(), entity);
return CreatedAtAction(nameof(GetById), new { id }, dto);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating scenario");
return StatusCode(500, new { error = "Failed to create scenario" });
}
}
///
/// Update existing scenario. Accepts Type + ConfigJson to avoid deserializing abstract TestScenario.
///
[HttpPut("{id:guid}")]
public async Task Update(Guid id, [FromBody] CreateOrUpdateScenarioRequest request)
{
try
{
var existing = await _scenarioRepository.GetEntityByIdAsync(id);
if (existing == null)
return NotFound(new { error = $"Scenario with ID {id} not found" });
var scenario = DeserializeScenarioFromRequest(request);
scenario.Id = id;
await _scenarioRepository.UpdateAsync(scenario);
return NoContent();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating scenario {Id}", id);
return StatusCode(500, new { error = "Failed to update scenario" });
}
}
///
/// Deserialize ConfigJson to concrete TestScenario based on Type (avoids abstract type deserialization).
///
private static TestScenario DeserializeScenarioFromRequest(CreateOrUpdateScenarioRequest request)
{
var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
TestScenario scenario = request.Type switch
{
TrajectoryType.StraightLine => JsonSerializer.Deserialize(request.ConfigJson, jsonOptions)
?? new StraightLineScenario(),
TrajectoryType.Circle => JsonSerializer.Deserialize(request.ConfigJson, jsonOptions)
?? new CircleScenario(),
TrajectoryType.Custom => JsonSerializer.Deserialize(request.ConfigJson, jsonOptions)
?? new CustomPathScenario(),
_ => throw new NotSupportedException($"Scenario type {request.Type} is not supported")
};
scenario.Id = request.Id ?? Guid.Empty;
scenario.Name = request.Name;
scenario.Description = request.Description;
scenario.Type = request.Type;
scenario.IsDefault = request.IsDefault;
return scenario;
}
///
/// Delete scenario. Only custom (non-default) scenarios can be deleted.
///
[HttpDelete("{id:guid}")]
public async Task Delete(Guid id)
{
try
{
var existing = await _scenarioRepository.GetByIdAsync(id);
if (existing == null)
return NotFound(new { error = $"Scenario with ID {id} not found" });
if (existing.IsDefault)
return BadRequest(new { error = "Cannot delete default scenario. Only custom scenarios can be deleted." });
await _scenarioRepository.DeleteAsync(id);
return NoContent();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting scenario {Id}", id);
return StatusCode(500, new { error = "Failed to delete scenario" });
}
}
}