program Test_CalculosTiempos;

{ ============================================================================
  PROYECTO: GESTION--RAUL ASENCIO
  MODULO:   Test_CalculosTiempos.dpr
  OBJETIVO: Test Runner Automatizado de Consola para uControlTiemposCalcService
            Verificacion en 4 Niveles (Tiers 1-4):
            - Tier 1: Cobertura Funcional de Calculo, Duracion, Costes y Formato
            - Tier 2: Casos Frontera (0 min, Sub-minuto, 24h, Medianoche, Contiguos, Abiertos)
            - Tier 3: Escenarios Multi-operario y Multi-seccion (Interaccion Cruzada)
            - Tier 4: Escenario Oficial de Aceptacion (Amasado: 5.5h y 88.50 EUR)
  ESTANDAR: Delphi 11 Alexandria / DCC32 / Pure Console (APPTYPE CONSOLE)
  SALIDA:   ExitCode 0 = 100% Pasadas | ExitCode 1 = Fallos detectados
  ============================================================================ }

{$APPTYPE CONSOLE}
// {$R *.res}

uses
  System.SysUtils,
  System.DateUtils,
  System.Math,
  uControlTiemposCalcService;

var
  GPassCount: Integer = 0;
  GFailCount: Integer = 0;

procedure AssertTrue(const ADescription: string; const ACondition: Boolean);
begin
  if ACondition then
  begin
    Writeln('  [OK] ', ADescription);
    Inc(GPassCount);
  end
  else
  begin
    Writeln('  [FALLO] ', ADescription);
    Inc(GFailCount);
  end;
end;

procedure AssertFalse(const ADescription: string; const ACondition: Boolean);
begin
  AssertTrue(ADescription, not ACondition);
end;

procedure AssertEqualsInt(const ADescription: string; const AExpected, AActual: Integer);
begin
  AssertTrue(
    Format('%s (Esperado: %d, Obtenido: %d)', [ADescription, AExpected, AActual]),
    AExpected = AActual
  );
end;

procedure AssertEqualsFloat(const ADescription: string; const AExpected, AActual: Double; const ATolerance: Double = 0.0001);
begin
  AssertTrue(
    Format('%s (Esperado: %.4f, Obtenido: %.4f)', [ADescription, AExpected, AActual]),
    Abs(AExpected - AActual) <= ATolerance
  );
end;

procedure AssertEqualsCurr(const ADescription: string; const AExpected, AActual: Currency);
begin
  AssertTrue(
    Format('%s (Esperado: %.2f EUR, Obtenido: %.2f EUR)', [ADescription, Double(AExpected), Double(AActual)]),
    AExpected = AActual
  );
end;

procedure AssertEqualsStr(const ADescription: string; const AExpected, AActual: string);
begin
  AssertTrue(
    Format('%s (Esperado: "%s", Obtenido: "%s")', [ADescription, AExpected, AActual]),
    SameText(AExpected, AActual)
  );
end;

{ ============================================================================
  TIER 1: COBERTURA FUNCIONAL DE SERVICIO
  ============================================================================ }

procedure RunTier1_CoberturaFuncional;
var
  LIni, LFin: TDateTime;
  LIntervalo: TIntervaloTiempo;
  LCoste: Currency;
  LFormato: string;
begin
  Writeln('======================================================================');
  Writeln('TIER 1: COBERTURA FUNCIONAL BASICA');
  Writeln('======================================================================');

  // 1.1 Calculo de duracion estandar
  LIni := EncodeDateTime(2026, 9, 4, 8, 0, 0, 0);
  LFin := EncodeDateTime(2026, 9, 4, 11, 30, 0, 0);
  LIntervalo := TControlTiemposCalcService.CalcularDuracion(LIni, LFin);
  AssertEqualsInt('T1.1.1 Duracion 08:00 a 11:30 en minutos', 210, LIntervalo.Minutos);
  AssertEqualsFloat('T1.1.2 Duracion 08:00 a 11:30 en horas decimales', 3.5000, LIntervalo.HorasDecimal);

  LIni := EncodeDateTime(2026, 9, 4, 9, 30, 0, 0);
  LFin := EncodeDateTime(2026, 9, 4, 11, 30, 0, 0);
  LIntervalo := TControlTiemposCalcService.CalcularDuracion(LIni, LFin);
  AssertEqualsInt('T1.1.3 Duracion 09:30 a 11:30 en minutos', 120, LIntervalo.Minutos);
  AssertEqualsFloat('T1.1.4 Duracion 09:30 a 11:30 en horas decimales', 2.0000, LIntervalo.HorasDecimal);

  // 1.2 Conversiones auxiliares Minutos <-> Horas
  AssertEqualsFloat('T1.2.1 MinutosAHorasDecimales(210)', 3.5000, TControlTiemposCalcService.MinutosAHorasDecimales(210));
  AssertEqualsFloat('T1.2.2 MinutosAHorasDecimales(45)', 0.7500, TControlTiemposCalcService.MinutosAHorasDecimales(45));
  AssertEqualsInt('T1.2.3 HorasDecimalesAMinutos(3.5)', 210, TControlTiemposCalcService.HorasDecimalesAMinutos(3.5));
  AssertEqualsInt('T1.2.4 HorasDecimalesAMinutos(0.75)', 45, TControlTiemposCalcService.HorasDecimalesAMinutos(0.75));

  // 1.3 Calculo economico de linea
  LCoste := TControlTiemposCalcService.CalcularCoste(210, 15.00);
  AssertEqualsCurr('T1.3.1 Coste 210 min @ 15.00 EUR/h', 52.50, LCoste);

  LCoste := TControlTiemposCalcService.CalcularCoste(120, 18.00);
  AssertEqualsCurr('T1.3.2 Coste 120 min @ 18.00 EUR/h', 36.00, LCoste);

  LCoste := TControlTiemposCalcService.CalcularCoste(20, 15.00);
  AssertEqualsCurr('T1.3.3 Precision exacta: 20 min @ 15.00 EUR/h = 5.00 EUR', 5.00, LCoste);

  LCoste := TControlTiemposCalcService.CalcularCoste(40, 14.50);
  AssertEqualsCurr('T1.3.4 Redondeo comercial: 40 min @ 14.50 EUR/h = 9.67 EUR', 9.67, LCoste);

  LCoste := TControlTiemposCalcService.CalcularCoste(45, 12.00);
  AssertEqualsCurr('T1.3.5 Coste 45 min @ 12.00 EUR/h = 9.00 EUR', 9.00, LCoste);

  // 1.4 Overload de horas decimales
  LCoste := TControlTiemposCalcService.CalcularCoste(3.5, 15.00);
  AssertEqualsCurr('T1.4.1 Overload HorasDecimal: 3.5h @ 15.00 EUR/h', 52.50, LCoste);

  LCoste := TControlTiemposCalcService.CalcularCoste(2.0, 18.00);
  AssertEqualsCurr('T1.4.2 Overload HorasDecimal: 2.0h @ 18.00 EUR/h', 36.00, LCoste);

  // 1.5 Validacion basica de solapamientos
  AssertFalse('T1.5.1 Intervalos disjuntos no solapan',
    TControlTiemposCalcService.ValidarSolapamiento(
      EncodeDateTime(2026, 9, 4, 8, 0, 0, 0), EncodeDateTime(2026, 9, 4, 10, 0, 0, 0),
      EncodeDateTime(2026, 9, 4, 10, 30, 0, 0), EncodeDateTime(2026, 9, 4, 12, 0, 0, 0)
    ));

  AssertTrue('T1.5.2 Intervalos con cruce parcial solapan',
    TControlTiemposCalcService.ValidarSolapamiento(
      EncodeDateTime(2026, 9, 4, 8, 0, 0, 0), EncodeDateTime(2026, 9, 4, 10, 30, 0, 0),
      EncodeDateTime(2026, 9, 4, 10, 0, 0, 0), EncodeDateTime(2026, 9, 4, 12, 0, 0, 0)
    ));

  // 1.6 Formateo de horas decimales
  LFormato := TControlTiemposCalcService.FormatearHorasDecimal(3.50);
  AssertTrue('T1.6.1 FormatearHorasDecimal(3.50) formato correcto', (LFormato = '3.50') or (LFormato = '3,50'));

  LFormato := TControlTiemposCalcService.FormatearHorasDecimal(0.00);
  AssertTrue('T1.6.2 FormatearHorasDecimal(0.00) formato correcto', (LFormato = '0.00') or (LFormato = '0,00'));

  Writeln('');
end;

{ ============================================================================
  TIER 2: CASOS FRONTERA Y CONDICIONES LIMITE (BOUNDARY & CORNER CASES)
  ============================================================================ }

procedure RunTier2_CasosFrontera;
var
  LIni1, LFin1, LIni2, LFin2: TDateTime;
  LIntervalo: TIntervaloTiempo;
  LCoste: Currency;
begin
  Writeln('======================================================================');
  Writeln('TIER 2: CASOS FRONTERA Y CONDICIONES LIMITE');
  Writeln('======================================================================');

  // 2.1 Intervalo de 0 minutos (Inicio = Fin)
  LIni1 := EncodeDateTime(2026, 9, 4, 8, 0, 0, 0);
  LFin1 := LIni1;
  LIntervalo := TControlTiemposCalcService.CalcularDuracion(LIni1, LFin1);
  AssertEqualsInt('T2.1.1 Duracion con Inicio = Fin da 0 minutos', 0, LIntervalo.Minutos);
  AssertEqualsFloat('T2.1.2 Horas con Inicio = Fin da 0.0000', 0.0, LIntervalo.HorasDecimal);
  AssertEqualsCurr('T2.1.3 Coste con 0 minutos da 0.00 EUR', 0.00, TControlTiemposCalcService.CalcularCoste(0, 15.00));

  // 2.2 Intervalo sub-minuto (30 segundos)
  LIni1 := EncodeDateTime(2026, 9, 4, 8, 0, 0, 0);
  LFin1 := EncodeDateTime(2026, 9, 4, 8, 0, 30, 0);
  LIntervalo := TControlTiemposCalcService.CalcularDuracion(LIni1, LFin1);
  AssertEqualsInt('T2.2.1 Duracion 30 segundos redondea a 0 minutos enteros', 0, LIntervalo.Minutos);

  // 2.3 Intervalo de 24 horas completas
  LIni1 := EncodeDateTime(2026, 9, 4, 8, 0, 0, 0);
  LFin1 := EncodeDateTime(2026, 9, 5, 8, 0, 0, 0);
  LIntervalo := TControlTiemposCalcService.CalcularDuracion(LIni1, LFin1);
  AssertEqualsInt('T2.3.1 Duracion 24h = 1440 minutos', 1440, LIntervalo.Minutos);
  AssertEqualsFloat('T2.3.2 Horas 24h = 24.0000', 24.0000, LIntervalo.HorasDecimal);
  AssertEqualsCurr('T2.3.3 Coste 24h @ 15.00 EUR/h = 360.00 EUR', 360.00, TControlTiemposCalcService.CalcularCoste(1440, 15.00));

  // 2.4 Cruce de medianoche (Turno nocturno 22:00 a 06:00 = 8.0h)
  LIni1 := EncodeDateTime(2026, 9, 4, 22, 0, 0, 0);
  LFin1 := EncodeDateTime(2026, 9, 5, 6, 0, 0, 0);
  LIntervalo := TControlTiemposCalcService.CalcularDuracion(LIni1, LFin1);
  AssertEqualsInt('T2.4.1 Cruce medianoche fecha completa: 480 minutos', 480, LIntervalo.Minutos);
  AssertEqualsFloat('T2.4.2 Cruce medianoche fecha completa: 8.0000 horas', 8.0000, LIntervalo.HorasDecimal);
  AssertEqualsCurr('T2.4.3 Cruce medianoche coste 8h @ 15.00 EUR/h = 120.00 EUR', 120.00, TControlTiemposCalcService.CalcularCoste(480, 15.00));

  // 2.4b Cruce de medianoche con solo horas (Trunc = 0)
  LIni1 := EncodeTime(22, 0, 0, 0);
  LFin1 := EncodeTime(6, 0, 0, 0);
  LIntervalo := TControlTiemposCalcService.CalcularDuracion(LIni1, LFin1);
  AssertEqualsInt('T2.4.4 Cruce medianoche time-only: 480 minutos', 480, LIntervalo.Minutos);
  AssertEqualsFloat('T2.4.5 Cruce medianoche time-only: 8.0000 horas', 8.0000, LIntervalo.HorasDecimal);

  // 2.4c Cruce de medianoche con misma fecha calendario (Trunc(AInicio) = Trunc(AFin) y AFin < AInicio)
  LIni1 := EncodeDateTime(2026, 9, 4, 22, 0, 0, 0);
  LFin1 := EncodeDateTime(2026, 9, 4, 6, 0, 0, 0);
  LIntervalo := TControlTiemposCalcService.CalcularDuracion(LIni1, LFin1);
  AssertEqualsInt('T2.4.6 Cruce medianoche misma fecha calendario: 480 minutos', 480, LIntervalo.Minutos);
  AssertEqualsFloat('T2.4.7 Cruce medianoche misma fecha calendario: 8.0000 horas', 8.0000, LIntervalo.HorasDecimal);

  // 2.5 Tarifas extremas (0.00 EUR y 999.99 EUR)
  AssertEqualsCurr('T2.5.1 Tarifa 0.00 EUR/h produce 0.00 EUR', 0.00, TControlTiemposCalcService.CalcularCoste(60, 0.00));
  AssertEqualsCurr('T2.5.2 Tarifa negativa produce 0.00 EUR', 0.00, TControlTiemposCalcService.CalcularCoste(60, -15.00));
  AssertEqualsCurr('T2.5.3 Minutos negativos producen 0.00 EUR', 0.00, TControlTiemposCalcService.CalcularCoste(-60, 15.00));

  LCoste := TControlTiemposCalcService.CalcularCoste(60, 999.99);
  AssertEqualsCurr('T2.5.4 Tarifa alta 60 min @ 999.99 EUR/h = 999.99 EUR', 999.99, LCoste);

  LCoste := TControlTiemposCalcService.CalcularCoste(30, 999.99);
  AssertEqualsCurr('T2.5.5 Tarifa alta 30 min @ 999.99 EUR/h = 500.00 EUR (redondeo comercial)', 500.00, LCoste);

  // 2.6 Intervalos estrictamente contiguos (E1 = S2 => NO solapa)
  LIni1 := EncodeDateTime(2026, 9, 4, 8, 0, 0, 0);
  LFin1 := EncodeDateTime(2026, 9, 4, 10, 0, 0, 0);
  LIni2 := EncodeDateTime(2026, 9, 4, 10, 0, 0, 0);
  LFin2 := EncodeDateTime(2026, 9, 4, 12, 0, 0, 0);
  AssertFalse('T2.6.1 Intervalos contiguos E1 = S2 NO solapan (cambio inmediato)',
    TControlTiemposCalcService.ValidarSolapamiento(LIni1, LFin1, LIni2, LFin2));
  AssertFalse('T2.6.2 Intervalos contiguos en orden inverso NO solapan',
    TControlTiemposCalcService.ValidarSolapamiento(LIni2, LFin2, LIni1, LFin1));

  // 2.7 Intervalos exactamente identicos (S1 = S2 y E1 = E2 => SOLAPA)
  AssertTrue('T2.7.1 Intervalos identicos SI solapan',
    TControlTiemposCalcService.ValidarSolapamiento(LIni1, LFin1, LIni1, LFin1));

  // 2.8 Intervalo subconjunto o contenido (B dentro de A => SOLAPA)
  LIni1 := EncodeDateTime(2026, 9, 4, 8, 0, 0, 0);
  LFin1 := EncodeDateTime(2026, 9, 4, 12, 0, 0, 0);
  LIni2 := EncodeDateTime(2026, 9, 4, 9, 0, 0, 0);
  LFin2 := EncodeDateTime(2026, 9, 4, 11, 0, 0, 0);
  AssertTrue('T2.8.1 Intervalo contenido B dentro de A SI solapa',
    TControlTiemposCalcService.ValidarSolapamiento(LIni1, LFin1, LIni2, LFin2));
  AssertTrue('T2.8.2 Intervalo contenedor A sobre B SI solapa',
    TControlTiemposCalcService.ValidarSolapamiento(LIni2, LFin2, LIni1, LFin1));

  // 2.9 Intervalos abiertos (Fin = 0 / NULL)
  LIni1 := EncodeDateTime(2026, 9, 4, 8, 0, 0, 0);
  LFin1 := 0; // Registro abierto en planta
  LIni2 := EncodeDateTime(2026, 9, 4, 9, 0, 0, 0);
  LFin2 := EncodeDateTime(2026, 9, 4, 11, 0, 0, 0);
  AssertTrue('T2.9.1 Intento de nuevo marcaje mientras hay uno abierto SI solapa',
    TControlTiemposCalcService.ValidarSolapamiento(LIni1, LFin1, LIni2, LFin2));

  // Intervalo previo cerrado antes de que el abierto comience -> NO solapa
  LIni2 := EncodeDateTime(2026, 9, 4, 6, 0, 0, 0);
  LFin2 := EncodeDateTime(2026, 9, 4, 7, 59, 0, 0);
  AssertFalse('T2.9.2 Marcaje anterior cerrado antes de inicio de abierto NO solapa',
    TControlTiemposCalcService.ValidarSolapamiento(LIni1, LFin1, LIni2, LFin2));

  // Dos registros abiertos simultaneamente -> SOLAPA
  AssertTrue('T2.9.3 Dos registros abiertos sin fin simultaneos SI solapan',
    TControlTiemposCalcService.ValidarSolapamiento(LIni1, 0, LIni2, 0));

  // 2.10 Fechas invertidas (Fin < Inicio en fechas completas con dias distintos)
  LIni1 := EncodeDateTime(2026, 9, 5, 12, 0, 0, 0);
  LFin1 := EncodeDateTime(2026, 9, 4, 10, 0, 0, 0);
  LIntervalo := TControlTiemposCalcService.CalcularDuracion(LIni1, LFin1);
  AssertEqualsInt('T2.10.1 Fecha fin menor que inicio devuelve 0 minutos', 0, LIntervalo.Minutos);
  AssertEqualsFloat('T2.10.2 Fecha fin menor que inicio devuelve 0.0000 horas', 0.0, LIntervalo.HorasDecimal);

  // 2.11 Solapamiento cruzado entre fechas adyacentes (Turno D-1 noche vs Turno D manana)
  AssertTrue('T2.11.1 Turno manana D colisiona con turno noche D-1 (05:00 a 06:00)',
    TControlTiemposCalcService.ValidarSolapamiento(
      EncodeDateTime(2026, 9, 4, 5, 0, 0, 0), EncodeDateTime(2026, 9, 4, 13, 0, 0, 0),
      EncodeDateTime(2026, 9, 3, 22, 0, 0, 0), EncodeDateTime(2026, 9, 4, 6, 0, 0, 0)
    ));

  AssertFalse('T2.11.2 Turno manana D contiguo a fin de turno noche D-1 a las 06:00 NO colisiona',
    TControlTiemposCalcService.ValidarSolapamiento(
      EncodeDateTime(2026, 9, 4, 6, 0, 0, 0), EncodeDateTime(2026, 9, 4, 14, 0, 0, 0),
      EncodeDateTime(2026, 9, 3, 22, 0, 0, 0), EncodeDateTime(2026, 9, 4, 6, 0, 0, 0)
    ));

  // 2.12 Solapamiento con turno abierto en planta
  AssertTrue('T2.12.1 Turno manual colisiona con turno en curso en planta (AFin = 0)',
    TControlTiemposCalcService.ValidarSolapamiento(
      EncodeDateTime(2026, 9, 4, 10, 0, 0, 0), EncodeDateTime(2026, 9, 4, 12, 0, 0, 0),
      EncodeDateTime(2026, 9, 4, 8, 0, 0, 0), 0
    ));

  AssertFalse('T2.12.2 Turno manual previo a inicio de turno en curso NO colisiona',
    TControlTiemposCalcService.ValidarSolapamiento(
      EncodeDateTime(2026, 9, 4, 6, 0, 0, 0), EncodeDateTime(2026, 9, 4, 7, 59, 0, 0),
      EncodeDateTime(2026, 9, 4, 8, 0, 0, 0), 0
    ));

  Writeln('');
end;

{ ============================================================================
  TIER 3: INTERACCION MULTI-OPERARIO Y MULTI-SECCION (CROSS-FEATURE / PAIRWISE)
  ============================================================================ }

procedure RunTier3_MultiOperarioMultiSeccion;
var
  // Operario 1 (15.00 EUR/h)
  LResOp1_SecAmasado: TResultadoCalculo;
  LResOp1_SecHorneado: TResultadoCalculo;
  // Operario 2 (18.00 EUR/h)
  LResOp2_SecAmasado: TResultadoCalculo;
  LResOp2_ZonaPicking: TResultadoCalculo;
  // Operario 3 (12.50 EUR/h)
  LResOp3_SecHorneado: TResultadoCalculo;

  // Agregados Secciones
  LHorasSecAmasado: Double;
  LCosteSecAmasado: Currency;
  LHorasSecHorneado: Double;
  LCosteSecHorneado: Currency;
  LHorasZonaPicking: Double;
  LCosteZonaPicking: Currency;

  // Agregados Globales
  LTotalMinutosGlobal: Integer;
  LTotalHorasGlobal: Double;
  LTotalCosteGlobal: Currency;

  // Agregados por Operario
  LTotalCosteOp1, LTotalCosteOp2, LTotalCosteOp3: Currency;
  LTotalHorasOp1, LTotalHorasOp2, LTotalHorasOp3: Double;
begin
  Writeln('======================================================================');
  Writeln('TIER 3: MULTI-OPERARIO Y MULTI-SECCION (PAIRWISE / WORKLOAD)');
  Writeln('======================================================================');

  // Operario 1: 08:00 - 11:30 en "Amasado" (3.5h @ 15.00 EUR/h)
  LResOp1_SecAmasado := TControlTiemposCalcService.CalcularResultado(
    EncodeDateTime(2026, 9, 4, 8, 0, 0, 0), EncodeDateTime(2026, 9, 4, 11, 30, 0, 0), 15.00);

  // Operario 1: 11:30 - 14:30 en "Horneado" (3.0h @ 15.00 EUR/h)
  LResOp1_SecHorneado := TControlTiemposCalcService.CalcularResultado(
    EncodeDateTime(2026, 9, 4, 11, 30, 0, 0), EncodeDateTime(2026, 9, 4, 14, 30, 0, 0), 15.00);

  // Verificar que los dos turnos consecutivos de Operario 1 no colisionan
  AssertFalse('T3.1.1 Operario 1: Cambio continuo Amasado -> Horneado a las 11:30 NO solapa',
    TControlTiemposCalcService.ValidarSolapamiento(
      EncodeDateTime(2026, 9, 4, 8, 0, 0, 0), EncodeDateTime(2026, 9, 4, 11, 30, 0, 0),
      EncodeDateTime(2026, 9, 4, 11, 30, 0, 0), EncodeDateTime(2026, 9, 4, 14, 30, 0, 0)
    ));

  // Operario 2: 09:30 - 11:30 en "Amasado" (2.0h @ 18.00 EUR/h)
  LResOp2_SecAmasado := TControlTiemposCalcService.CalcularResultado(
    EncodeDateTime(2026, 9, 4, 9, 30, 0, 0), EncodeDateTime(2026, 9, 4, 11, 30, 0, 0), 18.00);

  // Operario 2: 12:30 - 16:30 en "Picking" Almacen (4.0h @ 18.00 EUR/h)
  LResOp2_ZonaPicking := TControlTiemposCalcService.CalcularResultado(
    EncodeDateTime(2026, 9, 4, 12, 30, 0, 0), EncodeDateTime(2026, 9, 4, 16, 30, 0, 0), 18.00);

  // Operario 3: 06:00 - 10:00 en "Horneado" (4.0h @ 12.50 EUR/h)
  LResOp3_SecHorneado := TControlTiemposCalcService.CalcularResultado(
    EncodeDateTime(2026, 9, 4, 6, 0, 0, 0), EncodeDateTime(2026, 9, 4, 10, 0, 0, 0), 12.50);

  // Agregacion Seccion "Amasado"
  LHorasSecAmasado := LResOp1_SecAmasado.HorasDecimales + LResOp2_SecAmasado.HorasDecimales;
  LCosteSecAmasado := LResOp1_SecAmasado.CosteTotal + LResOp2_SecAmasado.CosteTotal;
  AssertEqualsFloat('T3.2.1 Seccion Amasado - Horas Totales (3.5h + 2.0h)', 5.5000, LHorasSecAmasado);
  AssertEqualsCurr('T3.2.2 Seccion Amasado - Coste Total (52.50 + 36.00)', 88.50, LCosteSecAmasado);

  // Agregacion Seccion "Horneado"
  LHorasSecHorneado := LResOp1_SecHorneado.HorasDecimales + LResOp3_SecHorneado.HorasDecimales;
  LCosteSecHorneado := LResOp1_SecHorneado.CosteTotal + LResOp3_SecHorneado.CosteTotal;
  AssertEqualsFloat('T3.3.1 Seccion Horneado - Horas Totales (3.0h + 4.0h)', 7.0000, LHorasSecHorneado);
  AssertEqualsCurr('T3.3.2 Seccion Horneado - Coste Total (45.00 + 50.00)', 95.00, LCosteSecHorneado);

  // Agregacion Zona Almacen "Picking"
  LHorasZonaPicking := LResOp2_ZonaPicking.HorasDecimales;
  LCosteZonaPicking := LResOp2_ZonaPicking.CosteTotal;
  AssertEqualsFloat('T3.4.1 Zona Picking - Horas Totales', 4.0000, LHorasZonaPicking);
  AssertEqualsCurr('T3.4.2 Zona Picking - Coste Total (4.0h * 18.00)', 72.00, LCosteZonaPicking);

  // Totales Globales del Dia
  LTotalMinutosGlobal := LResOp1_SecAmasado.MinutosTotales + LResOp1_SecHorneado.MinutosTotales +
                         LResOp2_SecAmasado.MinutosTotales + LResOp2_ZonaPicking.MinutosTotales +
                         LResOp3_SecHorneado.MinutosTotales;
  LTotalHorasGlobal := LHorasSecAmasado + LHorasSecHorneado + LHorasZonaPicking;
  LTotalCosteGlobal := LCosteSecAmasado + LCosteSecHorneado + LCosteZonaPicking;

  AssertEqualsInt('T3.5.1 Global Jornada - Minutos Totales (210+180+120+240+240)', 990, LTotalMinutosGlobal);
  AssertEqualsFloat('T3.5.2 Global Jornada - Horas Totales (5.5 + 7.0 + 4.0)', 16.5000, LTotalHorasGlobal);
  AssertEqualsCurr('T3.5.3 Global Jornada - Coste Total (88.50 + 95.00 + 72.00)', 255.50, LTotalCosteGlobal);

  // Verificacion de cuadre por eje trabajador
  LTotalHorasOp1 := LResOp1_SecAmasado.HorasDecimales + LResOp1_SecHorneado.HorasDecimales;
  LTotalCosteOp1 := LResOp1_SecAmasado.CosteTotal + LResOp1_SecHorneado.CosteTotal;
  AssertEqualsFloat('T3.6.1 Operario 1 - Horas Totales (3.5 + 3.0)', 6.5000, LTotalHorasOp1);
  AssertEqualsCurr('T3.6.2 Operario 1 - Coste Total (52.50 + 45.00)', 97.50, LTotalCosteOp1);

  LTotalHorasOp2 := LResOp2_SecAmasado.HorasDecimales + LResOp2_ZonaPicking.HorasDecimales;
  LTotalCosteOp2 := LResOp2_SecAmasado.CosteTotal + LResOp2_ZonaPicking.CosteTotal;
  AssertEqualsFloat('T3.6.3 Operario 2 - Horas Totales (2.0 + 4.0)', 6.0000, LTotalHorasOp2);
  AssertEqualsCurr('T3.6.4 Operario 2 - Coste Total (36.00 + 72.00)', 108.00, LTotalCosteOp2);

  LTotalHorasOp3 := LResOp3_SecHorneado.HorasDecimales;
  LTotalCosteOp3 := LResOp3_SecHorneado.CosteTotal;
  AssertEqualsFloat('T3.6.5 Operario 3 - Horas Totales', 4.0000, LTotalHorasOp3);
  AssertEqualsCurr('T3.6.6 Operario 3 - Coste Total', 50.00, LTotalCosteOp3);

  // Cuadre cruzado bidireccional (Suma por Secciones = Suma por Operarios)
  AssertEqualsCurr('T3.7.1 Cuadre de integridad: Suma por operarios == Suma por secciones',
    LTotalCosteGlobal, LTotalCosteOp1 + LTotalCosteOp2 + LTotalCosteOp3);

  Writeln('');
end;

{ ============================================================================
  TIER 4: ESCENARIO OFICIAL DE ACEPTACION (REAL-WORLD ACCEPTANCE SCENARIO)
  ============================================================================ }

procedure RunTier4_EscenarioAceptacionOficial;
var
  LInicioA, LFinA: TDateTime;
  LInicioB, LFinB: TDateTime;
  LResA, LResB: TResultadoCalculo;
  LHorasTotalesAmasado: Double;
  LCosteTotalAmasado: Currency;
begin
  Writeln('======================================================================');
  Writeln('TIER 4: ESCENARIO OFICIAL DE ACEPTACION');
  Writeln('Operario A (15.00 EUR/h, 3.5h) + Operario B (18.00 EUR/h, 2.0h) = 5.5h y 88.50 EUR');
  Writeln('======================================================================');

  // Configurar intervalos del caso de aceptacion:
  // Operario A: 08:00 a 11:30 (210 min = 3.5h), tarifa = 15.00 EUR/h
  LInicioA := EncodeDateTime(2026, 9, 4, 8, 0, 0, 0);
  LFinA := EncodeDateTime(2026, 9, 4, 11, 30, 0, 0);
  LResA := TControlTiemposCalcService.CalcularResultado(LInicioA, LFinA, 15.00);

  // Operario B: 09:30 a 11:30 (120 min = 2.0h), tarifa = 18.00 EUR/h
  LInicioB := EncodeDateTime(2026, 9, 4, 9, 30, 0, 0);
  LFinB := EncodeDateTime(2026, 9, 4, 11, 30, 0, 0);
  LResB := TControlTiemposCalcService.CalcularResultado(LInicioB, LFinB, 18.00);

  // Aserciones de Operario A
  AssertEqualsInt('T4.1.1 Operario A - Minutos exactos', 210, LResA.MinutosTotales);
  AssertEqualsFloat('T4.1.2 Operario A - Horas decimales exactas', 3.5000, LResA.HorasDecimales);
  AssertEqualsCurr('T4.1.3 Operario A - Tarifa horaria', 15.00, LResA.CosteHora);
  AssertEqualsCurr('T4.1.4 Operario A - Subtotal coste (3.5h * 15.00 EUR/h)', 52.50, LResA.CosteTotal);

  // Aserciones de Operario B
  AssertEqualsInt('T4.2.1 Operario B - Minutos exactos', 120, LResB.MinutosTotales);
  AssertEqualsFloat('T4.2.2 Operario B - Horas decimales exactas', 2.0000, LResB.HorasDecimales);
  AssertEqualsCurr('T4.2.3 Operario B - Tarifa horaria', 18.00, LResB.CosteHora);
  AssertEqualsCurr('T4.2.4 Operario B - Subtotal coste (2.0h * 18.00 EUR/h)', 36.00, LResB.CosteTotal);

  // Aserciones del Total de la Seccion "Amasado"
  LHorasTotalesAmasado := LResA.HorasDecimales + LResB.HorasDecimales;
  LCosteTotalAmasado := LResA.CosteTotal + LResB.CosteTotal;

  AssertEqualsFloat('T4.3.1 Seccion Amasado - Horas Totales de Aceptacion', 5.5000, LHorasTotalesAmasado);
  AssertEqualsCurr('T4.3.2 Seccion Amasado - Coste Total de Aceptacion', 88.50, LCosteTotalAmasado);

  // Formato decimal textual del resultado oficial
  AssertTrue('T4.3.3 Formato textual oficial 5.5h coincide con 5.50',
    (TControlTiemposCalcService.FormatearHorasDecimal(LHorasTotalesAmasado) = '5.50') or
    (TControlTiemposCalcService.FormatearHorasDecimal(LHorasTotalesAmasado) = '5,50'));

  Writeln('');
end;

{ ============================================================================
  PUNTO DE ENTRADA PRINCIPAL
  ============================================================================ }

begin
  try
    FormatSettings.DecimalSeparator := '.';
    Writeln('======================================================================');
    Writeln('  EJECUTOR DE PRUEBAS AUTOMATIZADAS: CONTROL DE TIEMPOS Y COSTES');
    Writeln('  GESTION--RAUL ASENCIO');
    Writeln('======================================================================');
    Writeln('');

    RunTier1_CoberturaFuncional;
    RunTier2_CasosFrontera;
    RunTier3_MultiOperarioMultiSeccion;
    RunTier4_EscenarioAceptacionOficial;

    Writeln('======================================================================');
    Writeln(Format('RESUMEN DE EJECUCION: %d Pasadas, %d Fallos (Total: %d)',
      [GPassCount, GFailCount, GPassCount + GFailCount]));
    Writeln('======================================================================');

    if GFailCount > 0 then
    begin
      Writeln('RESULTADO: [FALLO] Se detectaron aserciones no validas.');
      Halt(1);
    end
    else
    begin
      Writeln('RESULTADO: [EXITO] Todas las pruebas pasaron satisfactoriamente.');
      Halt(0);
    end;
  except
    on E: Exception do
    begin
      Writeln('ERROR FATAL EN TEST RUNNER: ', E.ClassName, ': ', E.Message);
      Halt(2);
    end;
  end;
end.
