using System.Reflection; using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Abstractions; using RutasDBUS.Components.Pages; using RutasDBUS.Modelos.Historial; using RutasDBUS.Modelos.Planificacion; using RutasDBUS.Servicios.Caminata; using RutasDBUS.Servicios.Configuracion; using RutasDBUS.Servicios.Planificacion; static void Check(bool result, string message) { if (!result) throw new Exception(message); Console.WriteLine("OK " + message); } var baseMethod = typeof(PlanificadorRutas).GetMethod("CrearConfiguracionBaseHistorial", BindingFlags.Static | BindingFlags.NonPublic)!; var defaults = (ConfiguracionHistorialRuta)baseMethod.Invoke(null, null)!; Check(defaults.PesoTransbordoRankingMinutos == 6, "Penalizacion inicial de 6 minutos"); var panelSinCambios = new PlanificadorRutas(); var alternarPanel = typeof(PlanificadorRutas).GetMethod( "ToggleConfigPanel", BindingFlags.Instance | BindingFlags.NonPublic)!; var solicitarCierre = typeof(PlanificadorRutas).GetMethod( "SolicitarCierreConfiguracionAvanzada", BindingFlags.Instance | BindingFlags.NonPublic)!; alternarPanel.Invoke(panelSinCambios, null); Check( solicitarCierre.Invoke(panelSinCambios, null) is true, "Configuracion avanzada sin cambios se cierra directamente"); var panelConCambios = new PlanificadorRutas(); alternarPanel.Invoke(panelConCambios, null); typeof(PlanificadorRutas) .GetField("_velocidadPieKmH", BindingFlags.Instance | BindingFlags.NonPublic)! .SetValue(panelConCambios, 4.2); Check( solicitarCierre.Invoke(panelConCambios, null) is false && (bool)typeof(PlanificadorRutas) .GetField("_mostrarConfirmacionCerrarConfiguracion", BindingFlags.Instance | BindingFlags.NonPublic)! .GetValue(panelConCambios)!, "Configuracion avanzada avisa antes de descartar cambios"); var folder = Path.Combine(Path.GetTempPath(), "rutasdbus-regression-" + Guid.NewGuid().ToString("N")); IConfiguration Config() => new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["ValoresFabrica:Directorio"] = folder }).Build(); ServicioValoresFabrica Service() => new(Config(), NullLogger.Instance); try { defaults.PesoTransbordoRankingMinutos = 9; defaults.FechaSimulada = null; defaults.HoraSimulada = null; Directory.CreateDirectory(folder); var file = Path.Combine(folder, "configuracion-fabrica.json"); await File.WriteAllTextAsync(file, JsonSerializer.Serialize(defaults)); var loaded = await Service().CargarAsync(); Check(loaded?.PesoTransbordoRankingMinutos == 9, "Valores compartidos entre instancias"); Check(loaded is { FechaSimulada: null, HoraSimulada: null }, "Valores antiguos sin fecha y hora siguen siendo validos"); var guardadaConFecha = defaults.Clonar(); guardadaConFecha.FechaSimulada = "2026-09-16"; guardadaConFecha.HoraSimulada = "13:40:00"; Check(await Service().GuardarAsync(guardadaConFecha), "Los valores de fabrica permiten guardar fecha y hora"); var cargadaConFecha = await Service().CargarAsync(); Check( cargadaConFecha is { FechaSimulada: "2026-09-16", HoraSimulada: "13:40:00" }, "Los valores de fabrica recuperan la fecha y hora guardadas"); var pagina = new PlanificadorRutas(); typeof(PlanificadorRutas) .GetField("_configuracionFabrica", BindingFlags.Instance | BindingFlags.NonPublic)! .SetValue(pagina, cargadaConFecha); var crearPredeterminada = typeof(PlanificadorRutas).GetMethod( "CrearConfiguracionPredeterminadaHistorial", BindingFlags.Instance | BindingFlags.NonPublic)!; var predeterminada = (ConfiguracionHistorialRuta)crearPredeterminada.Invoke(pagina, null)!; Check( predeterminada is { FechaSimulada: "2026-09-16", HoraSimulada: "13:40:00" }, "Restaurar conserva la fecha y hora de los valores de fabrica"); File.WriteAllText(file, "{incompleto"); Check(await Service().CargarAsync() is null, "JSON danado permite usar valores base"); } finally { // Solo datos temporales creados por esta prueba. if (Directory.Exists(folder)) Directory.Delete(folder, recursive: true); } var fake = new FakeValhalla(); var planner = new PlanificadorRutasServicio(null!, null!, fake, null!, null!, null!, null!); var walk = typeof(PlanificadorRutasServicio).GetMethod("WalkValhallaCached", BindingFlags.Instance | BindingFlags.NonPublic)!; async Task<(double[,]? linea, double distancia, double duracion)> Walk(List>? zones = null) => await (Task<(double[,]?, double, double)>)walk.Invoke(planner, new object?[] { 43.31, -1.99, 43.32, -1.98, zones, null })!; List Ring(double lon, double lat, double size) => new() { new[] { lon, lat }, new[] { lon + size, lat }, new[] { lon + size, lat + size }, new[] { lon, lat + size }, new[] { lon, lat } }; string Polyline6(params (double lat, double lon)[] puntos) { var texto = new System.Text.StringBuilder(); var latitudAnterior = 0; var longitudAnterior = 0; foreach (var punto in puntos) { Codificar((int)Math.Round(punto.lat * 1_000_000) - latitudAnterior, texto); Codificar((int)Math.Round(punto.lon * 1_000_000) - longitudAnterior, texto); latitudAnterior = (int)Math.Round(punto.lat * 1_000_000); longitudAnterior = (int)Math.Round(punto.lon * 1_000_000); } return texto.ToString(); static void Codificar(int diferencia, System.Text.StringBuilder texto) { var valor = diferencia < 0 ? ~(diferencia << 1) : diferencia << 1; while (valor >= 0x20) { texto.Append((char)((0x20 | (valor & 0x1f)) + 63)); valor >>= 5; } texto.Append((char)(valor + 63)); } } await Walk(); var far = new List> { Ring(-2.3, 43.0, .01) }; await Walk(far); Check(fake.Calls == 2 && fake.Zones == 1, "Toda zona activa se envia a V2 aunque este lejos de la ruta previa"); var near = new List> { Ring(-1.985, 43.315, .001), Ring(-1.982, 43.317, .001) }; await Walk(near); Check(fake.Zones == 2, "Se envian todos los poligonos"); await Walk(near); Check(fake.Calls == 3, "Mismas zonas reutilizan cache restringida"); near[0][0][0] += .00001; await Walk(near); Check(fake.Calls == 4, "Mover un vertice invalida esa entrada de cache"); await Walk(new() { Ring(-2.2, 43.1, .5) }); Check(fake.Calls == 5, "Una zona que rodea la ruta no se ignora aunque sus vertices esten lejos"); fake.Fail = true; var retry = new List> { Ring(-1.984, 43.312, .0002) }; await Walk(retry); fake.Fail = false; await Walk(retry); Check(fake.Calls == 7, "Un fallo temporal no queda guardado en cache de zonas"); var puntos = new List { new[] { 43.314, -1.984 }, new[] { 43.315, -1.983 } }; await (Task<(double[,]?, double, double)>)walk.Invoke(planner, new object?[] { 43.31, -1.99, 43.32, -1.98, near, puntos })!; Check(fake.Zones == 2 && fake.Points == 2, "Se envian juntos todos los poligonos y puntos activos"); var retryHandler = new RetryValhallaHandler(); var retryConfig = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Caminata:Valhalla:BaseUrl"] = "http://v2.test" }).Build(); var retryClient = new ClienteValhalla(new HttpClient(retryHandler), retryConfig); await retryClient.ObtenerRutaAsync( 43.316344, -1.983193, 43.315282, -1.985457, excludePolygons: new() { Ring(-1.9848, 43.3158, .0001) }); Check(retryHandler.Bodies.Count == 8, "V2 amplia progresivamente la busqueda y prueba la alternativa puntual cuando recibe 442"); using (var primerIntento = JsonDocument.Parse(retryHandler.Bodies[0])) using (var segundoIntento = JsonDocument.Parse(retryHandler.Bodies[1])) using (var tercerIntento = JsonDocument.Parse(retryHandler.Bodies[2])) using (var cuartoIntento = JsonDocument.Parse(retryHandler.Bodies[3])) { var primeraUbicacionInicial = primerIntento.RootElement.GetProperty("locations")[0]; var primeraUbicacionReintento = segundoIntento.RootElement.GetProperty("locations")[0]; var primeraUbicacionSegundoReintento = tercerIntento.RootElement.GetProperty("locations")[0]; var primeraUbicacionTercerReintento = cuartoIntento.RootElement.GetProperty("locations")[0]; Check(!primeraUbicacionInicial.TryGetProperty("radius", out _) && primeraUbicacionReintento.GetProperty("radius").GetInt32() == 50 && primeraUbicacionSegundoReintento.GetProperty("radius").GetInt32() == 100 && primeraUbicacionTercerReintento.GetProperty("radius").GetInt32() == 200, "Los reintentos buscan una arista alternativa hasta un radio controlado de 200 metros"); } var polygonFallbackHandler = new PolygonNoPathThenPointSuccessHandler( Polyline6((43.31, -1.99), (43.32, -1.98)), Polyline6((43.31, -1.99), (43.31, -1.98), (43.32, -1.98))); var polygonFallbackClient = new ClienteValhalla(new HttpClient(polygonFallbackHandler), retryConfig); var rutaConPoligonoConvertido = await polygonFallbackClient.ObtenerRutaAsync( 43.31, -1.99, 43.32, -1.98, excludePolygons: new() { Ring(-1.9855, 43.3145, .001) }); Check(rutaConPoligonoConvertido.Linea is not null && polygonFallbackHandler.Bodies.Count == 6, "Un poligono que devuelve 442 se sustituye por puntos y encuentra una alternativa"); using (var peticionConPuntos = JsonDocument.Parse(polygonFallbackHandler.Bodies[^1])) { Check(peticionConPuntos.RootElement.GetProperty("exclude_polygons").ValueKind == JsonValueKind.Null && peticionConPuntos.RootElement.GetProperty("exclude_locations").GetArrayLength() > 0, "El fallback conserva la zona como validacion y envia a v2 las aristas afectadas"); } var poligonoValidacion = Ring(-1.9855, 43.3145, .001); var validationHandler = new PolygonValidationValhallaHandler( Polyline6((43.31, -1.99), (43.32, -1.98)), Polyline6((43.31, -1.99), (43.31, -1.98), (43.32, -1.98))); var validationClient = new ClienteValhalla(new HttpClient(validationHandler), retryConfig); var rutaValidada = await validationClient.ObtenerRutaAsync( 43.31, -1.99, 43.32, -1.98, excludePolygons: new() { poligonoValidacion }); Check(validationHandler.Bodies.Count == 2 && rutaValidada.Linea is not null, "V2 recalcula una ruta que atraviesa una zona activa"); using (var peticionReforzada = JsonDocument.Parse(validationHandler.Bodies[1])) { Check(peticionReforzada.RootElement.GetProperty("exclude_locations").GetArrayLength() > 0, "La ruta conflictiva refuerza la exclusion sobre las aristas atravesadas"); } var parametrosConExclusion = new ParametrosPlanificadorRuta { UsarValhallaParaCaminatas = true, PoligonosExclusionCaminata = new() { Ring(-1.985, 43.315, .001) } }; var caminataNoResuelta = typeof(PlanificadorRutasServicio).GetMethod( "CaminataNoResueltaConExclusiones", BindingFlags.Static | BindingFlags.NonPublic)!; bool RechazarCaminata(ParametrosPlanificadorRuta parametros, double directa, double[,]? linea, double distancia) => (bool)caminataNoResuelta.Invoke(null, new object?[] { parametros, directa, linea, distancia })!; Check(RechazarCaminata(parametrosConExclusion, 100, null, 0), "Una caminata V2 sin ruta se descarta cuando hay exclusiones"); Check(!RechazarCaminata(new ParametrosPlanificadorRuta { UsarValhallaParaCaminatas = true }, 100, null, 0), "Sin exclusiones se conserva el fallback historico"); Check(!RechazarCaminata(parametrosConExclusion, 100, new[,] { { 43.31, -1.99 }, { 43.32, -1.98 } }, 120), "Una caminata V2 valida se conserva con exclusiones"); var puntoDentro = typeof(PlanificadorRutasServicio).GetMethod( "PuntoDentroDePoligonoExclusion", BindingFlags.Static | BindingFlags.NonPublic)!; Check((bool)puntoDentro.Invoke(null, new object?[] { 43.3155, -1.9845, parametrosConExclusion.PoligonosExclusionCaminata })!, "Las paradas interiores a una zona se detectan antes de calcular"); var page = new PlanificadorRutas(); var flags = BindingFlags.Instance | BindingFlags.NonPublic; var fabricaHistorial = (ConfiguracionHistorialRuta)baseMethod.Invoke(null, null)!; fabricaHistorial.FactorEquilibradoAndarBus = 6; fabricaHistorial.UsarHoraSimulada = true; fabricaHistorial.FechaSimulada = "2026-09-16"; fabricaHistorial.HoraSimulada = "13:40:00"; typeof(PlanificadorRutas).GetField("_configuracionFabrica", flags)!.SetValue(page, fabricaHistorial); var crearPredeterminadaHistorial = typeof(PlanificadorRutas).GetMethod( "CrearConfiguracionPredeterminadaParaHistorial", flags)!; var crearGuardadaHistorial = typeof(PlanificadorRutas).GetMethod( "CrearConfiguracionGuardadaHistorial", flags)!; var configuracionGuardada = fabricaHistorial.Clonar(); configuracionGuardada.UsarHoraSimulada = true; configuracionGuardada.FechaSimulada = "2026-04-08"; configuracionGuardada.HoraSimulada = "08:46:17"; configuracionGuardada.FactorEquilibradoAndarBus = 99; var registroConHora = new RegistroHistorialRuta { Configuracion = configuracionGuardada }; var predeterminadaHistorial = (ConfiguracionHistorialRuta)crearPredeterminadaHistorial.Invoke(page, null)!; Check(predeterminadaHistorial.UsarHoraSimulada && predeterminadaHistorial.FechaSimulada == "2026-09-16" && predeterminadaHistorial.HoraSimulada == "13:40:00" && predeterminadaHistorial.FactorEquilibradoAndarBus == 6, "Predeterminada usa tambien la fecha y hora de fabrica"); var guardada = (ConfiguracionHistorialRuta)crearGuardadaHistorial.Invoke( page, new object[] { registroConHora })!; Check(guardada.UsarHoraSimulada && guardada.FechaSimulada == "2026-04-08" && guardada.HoraSimulada == "08:46:17" && guardada.FactorEquilibradoAndarBus == 99, "Guardada conserva la fecha, hora y parametros del historial"); var paginaCierreHistorial = new PlanificadorRutas(); typeof(PlanificadorRutas).GetField("_mostrarHistorialRutas", flags)! .SetValue(paginaCierreHistorial, true); typeof(PlanificadorRutas).GetField("_factorEquilibradoAndarBus", flags)! .SetValue(paginaCierreHistorial, 99.0); var configuracionAnteriorHistorial = fabricaHistorial.Clonar(); configuracionAnteriorHistorial.FactorEquilibradoAndarBus = 6; typeof(PlanificadorRutas).GetField("_configuracionTemporalPreviaHistorial", flags)! .SetValue(paginaCierreHistorial, configuracionAnteriorHistorial); var alternarHistorial = typeof(PlanificadorRutas).GetMethod("ToggleHistorialPanel", flags)!; await (Task)alternarHistorial.Invoke(paginaCierreHistorial, null)!; Check( !(bool)typeof(PlanificadorRutas).GetField("_mostrarHistorialRutas", flags)! .GetValue(paginaCierreHistorial)! && (double)typeof(PlanificadorRutas).GetField("_factorEquilibradoAndarBus", flags)! .GetValue(paginaCierreHistorial)! == 99.0, "Cerrar el historial conserva la configuracion de la ruta visible"); typeof(PlanificadorRutas).GetField("_factorEquilibradoAndarBus", flags)!.SetValue(page, 6.0); typeof(PlanificadorRutas).GetField("_factorPenalizacionDistanciaPie", flags)!.SetValue(page, 1.8); typeof(PlanificadorRutas).GetField("_parametroPenalizacionDistanciaPieMetros", flags)!.SetValue(page, 200.0); typeof(PlanificadorRutas).GetField("_pesoTransbordoRankingMinutos", flags)!.SetValue(page, 6.0); typeof(PlanificadorRutas).GetField("_velocidadBusKmH", flags)!.SetValue(page, 16.0); typeof(PlanificadorRutas).GetField("_mostrarElevacionesRuta", flags)!.SetValue(page, true); var alternativaDesglose = new AlternativaRuta { AlternativaBus = new RutaBusAlternativa { DistanciaTotalBus = 6000 }, DistanciasPiePorTramoAprox = new List { 100, 300 }, EsfuerzoExtraPendienteMetros = 25, NumeroTransbordosReales = 1 }; var calcularScore = typeof(PlanificadorRutas).GetMethod("CalcularScoreEquilibradoFactor", flags)!; var crearDesglose = typeof(PlanificadorRutas).GetMethod("CrearDesgloseScoreEquilibrado", flags)!; var scoreCalculado = (double)calcularScore.Invoke(page, new object?[] { alternativaDesglose })!; var desgloseCalculado = crearDesglose.Invoke(page, new object?[] { alternativaDesglose })!; var scoreDesglosado = (double)desgloseCalculado.GetType().GetProperty("ScoreTotal")! .GetValue(desgloseCalculado)!; Check(Math.Abs(scoreCalculado - scoreDesglosado) < 0.0001, "El desglose y el ranking usan exactamente el mismo score"); var formatearEtiqueta = typeof(PlanificadorRutas).GetMethod("FormatearEtiquetaAlternativaTarjeta", flags)!; Check((string)formatearEtiqueta.Invoke(page, new object[] { new AlternativaRuta { Etiqueta = "Equilibrada" }, 0 })! == "Opción 1", "Las alternativas de bus se muestran numeradas sin etiquetas de criterio"); Check((string)formatearEtiqueta.Invoke(page, new object[] { new AlternativaRuta { EsSoloAPie = true, Etiqueta = "Solo a pie (v2)" }, 3 })! == "Solo a pie (v2)", "Las dos alternativas solo a pie conservan su identificacion"); var maximoExclusiones = typeof(PlanificadorRutas).GetField( "MaximoExclusionesCaminata", BindingFlags.Static | BindingFlags.NonPublic)!; Check((int)maximoExclusiones.GetRawConstantValue()! == 25, "Las exclusiones quedan limitadas a 25 elementos"); typeof(PlanificadorRutas).GetField("_configuracionFabrica", flags)!.SetValue(page, defaults.Clonar()); typeof(PlanificadorRutas).GetField("_activarRangoLineas", flags)!.SetValue(page, true); typeof(PlanificadorRutas).GetField("_lineaMin", flags)!.SetValue(page, 40); typeof(PlanificadorRutas).GetField("_lineaMax", flags)!.SetValue(page, 40); typeof(PlanificadorRutas).GetField("_activarRangoItinerarios", flags)!.SetValue(page, false); typeof(PlanificadorRutas).GetField("_itinerarioMin", flags)!.SetValue(page, 7); typeof(PlanificadorRutas).GetField("_itinerarioMax", flags)!.SetValue(page, 7); typeof(PlanificadorRutas).GetMethod("AplicarValoresFabricaConservandoSeleccion", flags)!.Invoke(page, null); Check(!(bool)typeof(PlanificadorRutas).GetField("_activarRangoLineas", flags)!.GetValue(page)! && (int)typeof(PlanificadorRutas).GetField("_lineaMin", flags)!.GetValue(page)! == defaults.LineaMin && (int)typeof(PlanificadorRutas).GetField("_lineaMax", flags)!.GetValue(page)! == defaults.LineaMax && (bool)typeof(PlanificadorRutas).GetField("_activarRangoItinerarios", flags)!.GetValue(page)! && (int)typeof(PlanificadorRutas).GetField("_itinerarioMin", flags)!.GetValue(page)! == defaults.ItinerarioMin && (int)typeof(PlanificadorRutas).GetField("_itinerarioMax", flags)!.GetValue(page)! == defaults.ItinerarioMax, "Restaurar aplica tambien los valores de lineas e itinerarios"); var campoModoHistorial = typeof(PlanificadorRutas).GetField("_modoConfiguracionHistorial", flags)!; var modoGuardada = Enum.Parse(campoModoHistorial.FieldType, "Guardada"); campoModoHistorial.SetValue(page, modoGuardada); typeof(PlanificadorRutas).GetMethod("AplicarValoresFabricaConservandoSeleccion", flags)!.Invoke(page, null); Check(Equals(campoModoHistorial.GetValue(page), modoGuardada), "Restaurar valores conserva el modo de configuracion del historial"); var options = (List)typeof(PlanificadorRutas).GetField("_opcionesRuta", flags)!.GetValue(page)!; var selected = typeof(PlanificadorRutas).GetField("_indiceAlternativaSeleccionada", flags)!; var remember = typeof(PlanificadorRutas).GetMethod("ObtenerProveedorSoloAPieParaRecalculo", flags)!; string? Remember(bool keep) => (string?)remember.Invoke(page, new object[] { keep }); options.Add(new() { EsSoloAPie = true, ProveedorRuta = "v1" }); options.Add(new() { EsSoloAPie = true, ProveedorRuta = "v2" }); selected.SetValue(page, 1); Check(Remember(true) == "v2", "Se conserva la version andando seleccionada"); options.Clear(); Check(Remember(true) == "v2", "Otro cambio de punto durante el calculo conserva v2"); options.Add(new() { EsSoloAPie = false }); selected.SetValue(page, 0); Check(Remember(true) is null, "Seleccionar bus libera la preferencia andando"); options.Clear(); Check(Remember(false) is null, "Nueva seleccion de criterio no hereda la preferencia andando"); var osmHandler = new OsmWidthHandler(); using var osmCache = new MemoryCache(new MemoryCacheOptions()); var osmConfig = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Caminata:Osm:BaseUrl"] = "https://osm.test/api/0.6" }).Build(); var osmClient = new ClienteAtributosOsm(new HttpClient(osmHandler), osmCache, osmConfig); var anchos = await osmClient.ObtenerAnchurasAsync([101, 102, 103]); Check(anchos[101] == new AnchuraViaOsm("4.5", false) && anchos[102] == new AnchuraViaOsm("2", true) && !anchos.ContainsKey(103), "La anchura exacta y la estimada se leen de las etiquetas OSM"); await osmClient.ObtenerAnchurasAsync([101, 102, 103]); Check(osmHandler.Calls == 1, "Las anchuras OSM se reutilizan desde cache"); var formatearSuperficieSinDatos = typeof(PlanificadorRutas).GetMethod( "FormatearSuperficieSegmento", BindingFlags.Static | BindingFlags.NonPublic)!; var superficieSinDatos = (string)formatearSuperficieSinDatos.Invoke(null, [null])!; Check(superficieSinDatos.Contains("Carriles: -") && superficieSinDatos.Contains("Anchura: -"), "El tooltip mantiene todos los atributos cuando no hay datos de via"); Console.WriteLine("Pruebas completadas."); sealed class FakeValhalla : IClienteValhalla { public Task ObtenerInformacionSueloAsync( double[,] linea, CancellationToken cancellationToken = default) => Task.FromResult(null); public int Calls { get; private set; } public int Zones { get; private set; } public int Points { get; private set; } public bool Fail { get; set; } public Task<(double[,]? Linea, double DistanciaMetros, double DuracionSegundos)> ObtenerRutaAsync( double latitudDesde, double longitudDesde, double latitudHasta, double longitudHasta, string costing = "pedestrian", List>? excludePolygons = null, List? excludeLocations = null) { Calls++; Zones = excludePolygons?.Count ?? 0; Points = excludeLocations?.Count ?? 0; return Task.FromResult<(double[,]?, double, double)>(Fail ? (null, 0, 0) : (new[,] { { latitudDesde, longitudDesde }, { latitudHasta, longitudHasta } }, 100, 100)); } } sealed class OsmWidthHandler : HttpMessageHandler { public int Calls { get; private set; } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Calls++; const string contenido = """ { "elements": [ { "type": "way", "id": 101, "tags": { "width": "4.5" } }, { "type": "way", "id": 102, "tags": { "est_width": "2" } }, { "type": "way", "id": 103, "tags": { "highway": "residential" } } ] } """; return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(contenido, System.Text.Encoding.UTF8, "application/json") }); } } sealed class RetryValhallaHandler : HttpMessageHandler { public List Bodies { get; } = new(); protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Bodies.Add(await request.Content!.ReadAsStringAsync(cancellationToken)); return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) { Content = new StringContent( "{\"error_code\":442,\"error\":\"No path could be found for input\"}", System.Text.Encoding.UTF8, "application/json") }; } } sealed class PolygonValidationValhallaHandler(string primeraForma, string segundaForma) : HttpMessageHandler { public List Bodies { get; } = new(); protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Bodies.Add(await request.Content!.ReadAsStringAsync(cancellationToken)); var forma = Bodies.Count == 1 ? primeraForma : segundaForma; var contenido = JsonSerializer.Serialize(new { trip = new { units = "kilometers", summary = new { length = 1.0, time = 600.0 }, legs = new[] { new { shape = forma } } } }); return new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(contenido, System.Text.Encoding.UTF8, "application/json") }; } } sealed class PolygonNoPathThenPointSuccessHandler(string formaConflictiva, string formaAlternativa) : HttpMessageHandler { public List Bodies { get; } = new(); protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var cuerpo = await request.Content!.ReadAsStringAsync(cancellationToken); Bodies.Add(cuerpo); using var documento = JsonDocument.Parse(cuerpo); var raiz = documento.RootElement; var tienePoligonos = raiz.TryGetProperty("exclude_polygons", out var poligonos) && poligonos.ValueKind == JsonValueKind.Array && poligonos.GetArrayLength() > 0; if (tienePoligonos) { return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) { Content = new StringContent( "{\"error_code\":442,\"error\":\"No path could be found for input\"}", System.Text.Encoding.UTF8, "application/json") }; } var tienePuntos = raiz.TryGetProperty("exclude_locations", out var puntos) && puntos.ValueKind == JsonValueKind.Array && puntos.GetArrayLength() > 0; var contenido = JsonSerializer.Serialize(new { trip = new { units = "kilometers", summary = new { length = 1.0, time = 600.0 }, legs = new[] { new { shape = tienePuntos ? formaAlternativa : formaConflictiva } } } }); return new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(contenido, System.Text.Encoding.UTF8, "application/json") }; } }