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