package com.clanstop.healing.hooks;

import com.clanstop.healing.config.FrameworkConfig;
import com.clanstop.healing.driver.HealingContext;
import com.clanstop.healing.persistence.Db;
import com.clanstop.healing.persistence.RunLogDao;
import io.cucumber.plugin.ConcurrentEventListener;
import io.cucumber.plugin.event.EventPublisher;
import io.cucumber.plugin.event.PickleStepTestStep;
import io.cucumber.plugin.event.Status;
import io.cucumber.plugin.event.TestCaseStarted;
import io.cucumber.plugin.event.TestRunFinished;
import io.cucumber.plugin.event.TestRunStarted;
import io.cucumber.plugin.event.TestStepFinished;
import io.cucumber.plugin.event.TestStepStarted;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * The "plug and play" logging layer: attach this plugin to the runner and every step's
 * pass/fail, timing and error is persisted to PostgreSQL automatically — step authors write
 * no logging code. It also stamps each RUNNING step's row id into {@link HealingContext} so a
 * heal that fires inside a step FK-links back to it.
 *
 * <p>A RUNNING row is inserted on step start and updated on step finish, because the heal
 * (which needs the row id) happens <em>during</em> the step, before its pass/fail is known.
 */
public final class StepLoggingPlugin implements ConcurrentEventListener {

    private static final Logger log = LoggerFactory.getLogger(StepLoggingPlugin.class);

    private final RunLogDao dao = new RunLogDao();
    private final AtomicBoolean anyFailed = new AtomicBoolean(false);

    private final ThreadLocal<String> feature = ThreadLocal.withInitial(() -> "");
    private final ThreadLocal<String> scenario = ThreadLocal.withInitial(() -> "");
    private final ThreadLocal<Integer> ordinal = ThreadLocal.withInitial(() -> 0);
    private final ThreadLocal<Long> stepRowId = ThreadLocal.withInitial(() -> -1L);

    @Override
    public void setEventPublisher(EventPublisher publisher) {
        publisher.registerHandlerFor(TestRunStarted.class, this::onRunStarted);
        publisher.registerHandlerFor(TestCaseStarted.class, this::onCaseStarted);
        publisher.registerHandlerFor(TestStepStarted.class, this::onStepStarted);
        publisher.registerHandlerFor(TestStepFinished.class, this::onStepFinished);
        publisher.registerHandlerFor(TestRunFinished.class, this::onRunFinished);
    }

    private void onRunStarted(TestRunStarted e) {
        long runId = dao.createRun();
        HealingContext.setRunId(runId);
        if (runId >= 0) {
            log.info("Self-healing run #{} started.", runId);
        }
    }

    private void onCaseStarted(TestCaseStarted e) {
        feature.set(shortName(e.getTestCase().getUri().toString()));
        scenario.set(e.getTestCase().getName());
        ordinal.set(0);
    }

    private void onStepStarted(TestStepStarted e) {
        if (e.getTestStep() instanceof PickleStepTestStep step) {
            int ord = ordinal.get() + 1;
            ordinal.set(ord);
            long id = dao.startStep(HealingContext.runId(), feature.get(), scenario.get(),
                    step.getStep().getText(), ord);
            stepRowId.set(id);
            HealingContext.setCurrentStepId(id);
        }
    }

    private void onStepFinished(TestStepFinished e) {
        if (e.getTestStep() instanceof PickleStepTestStep) {
            Status status = e.getResult().getStatus();
            String error = e.getResult().getError() != null
                    ? e.getResult().getError().toString() : null;

            String screenshot = null;
            if (status == Status.FAILED) {
                anyFailed.set(true);
                FrameworkConfig cfg = FrameworkConfig.get();
                var driver = HealingContext.currentDriver();
                if (cfg.screenshotOnFailure() && driver != null) {
                    screenshot = Screenshots.capture(driver, cfg.screenshotDir(),
                            feature.get() + "_" + scenario.get() + "_step" + ordinal.get());
                    if (screenshot != null) {
                        log.info("Saved failure screenshot: {}", screenshot);
                    }
                }
            }
            dao.finishStep(stepRowId.get(), status.name(),
                    e.getResult().getDuration().toMillis(), error, screenshot);
            HealingContext.clearCurrentStep();
        }
    }

    private void onRunFinished(TestRunFinished e) {
        dao.finishRun(HealingContext.runId(), anyFailed.get() ? "FAILED" : "PASSED");
        Db.close();
    }

    private static String shortName(String uri) {
        int slash = uri.lastIndexOf('/');
        return slash >= 0 ? uri.substring(slash + 1) : uri;
    }
}
