/*
 * Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.pkl.core.stdlib.test.report;

import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import org.pkl.core.TestResults;
import org.pkl.core.TestResults.TestResult;
import org.pkl.core.util.AnsiStringBuilder;
import org.pkl.core.util.AnsiStringBuilder.AnsiCode;
import org.pkl.core.util.AnsiTheme;
import org.pkl.core.util.StringUtils;

public abstract class BaseReporter implements TestReporter {
  protected static final String passingMark = "✔ ";
  protected static final String failingMark = "✘ ";

  protected final boolean useColor;

  public BaseReporter(boolean useColor) {
    this.useColor = useColor;
  }

  @Override
  public void summarize(List<TestResults> allTestResults, Writer writer) throws IOException {
    var totalTests = 0;
    var totalFailedTests = 0;
    var totalAsserts = 0;
    var totalFailedAsserts = 0;
    var isFailed = false;
    var isExampleWrittenFailure = true;
    for (var testResults : allTestResults) {
      if (!isFailed) {
        isFailed = testResults.failed();
      }
      if (testResults.failed()) {
        isExampleWrittenFailure = testResults.isExampleWrittenFailure() & isExampleWrittenFailure;
      }
      totalTests += testResults.totalTests();
      totalFailedTests += testResults.totalFailures();
      totalAsserts += testResults.totalAsserts();
      totalFailedAsserts += testResults.totalAssertsFailed();
    }
    var builder = new AnsiStringBuilder(useColor);
    if (isFailed && isExampleWrittenFailure) {
      builder.append(totalFailedTests).append(" examples written");
    } else {
      makeStatsLine(builder, "tests", totalTests, totalFailedTests, isFailed);
      builder.append(", ");
      makeStatsLine(builder, "asserts", totalAsserts, totalFailedAsserts, isFailed);
    }
    builder.append('\n');
    writer.append(builder.toString());
  }

  protected void reportResult(TestResult result, AnsiStringBuilder builder) {
    builder.append("    ");

    if (result.isExampleWritten()) {
      builder.append("✍️ ").append(result.name());
    } else {
      if (result.isFailure()) {
        builder.append(AnsiTheme.FAILING_TEST_MARK, failingMark);
      } else {
        builder.append(AnsiTheme.PASSING_TEST_MARK, passingMark);
      }
      builder.append(AnsiTheme.TEST_NAME, result.name());
      if (result.isFailure()) {
        var failurePadding = "       ";
        builder.append("\n");
        StringUtils.joinToStringBuilder(
            builder,
            result.failures(),
            "\n",
            failure -> appendPadded(builder, failure.message(), failurePadding));
        StringUtils.joinToStringBuilder(
            builder,
            result.errors(),
            "\n",
            error -> appendPadded(builder, error.exception().getMessage(), failurePadding));
      }
    }
  }

  protected static void appendPadded(AnsiStringBuilder builder, String lines, String padding) {
    StringUtils.joinToStringBuilder(
        builder,
        lines.lines().collect(Collectors.toList()),
        "\n",
        str -> {
          if (!str.isEmpty()) builder.append(padding).append(str);
        });
  }

  private void makeStatsLine(
      AnsiStringBuilder sb, String kind, int total, int failed, boolean isFailed) {
    var passed = total - failed;
    var passRate = "0.0";
    if (total > 0) {
      var passedDecimal = new BigDecimal(passed);
      var hundred = new BigDecimal(100);
      var totalPrecise = new BigDecimal(total);
      passRate =
          hundred.multiply(passedDecimal).divide(totalPrecise, 1, RoundingMode.DOWN).toString();
    }
    var passRateLine = String.format(Locale.ROOT, "%s%% %s pass", passRate, kind);

    var color = isFailed ? AnsiCode.RED : AnsiCode.GREEN;
    sb.append(color, () -> sb.append(passRateLine));

    if (isFailed) {
      sb.append(" [").append(failed).append('/').append(total).append(" failed]");
    } else {
      sb.append(" [").append(passed).append(" passed]");
    }
  }
}
