package com.example.support;

import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.consumer.PactTestExecutionContext;
import au.com.dius.pact.consumer.PactVerificationResult;
import au.com.dius.pact.consumer.model.MockProviderConfig;
import au.com.dius.pact.core.model.DefaultPactReader;
import au.com.dius.pact.core.model.Interaction;
import au.com.dius.pact.core.model.Pact;
import au.com.dius.pact.core.model.PactSpecVersion;
import au.com.dius.pact.core.model.RequestResponseInteraction;
import au.com.dius.pact.core.model.RequestResponsePact;

import java.io.File;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Map;

import static au.com.dius.pact.consumer.ConsumerPactBuilder.consumer;
import static au.com.dius.pact.consumer.ConsumerPactRunnerKt.runConsumerTest;

/**
 * Owned consumer-driven contract testing with Pact (L3git). Two sides, both driven from plain
 * Cucumber steps - no annotations, no JUnit Pact extension:
 *
 * <ul>
 *   <li><b>Consumer</b> ({@link #defineContract}) - describe one interaction (a request your app
 *       makes and the response it expects). Pact stands up its own in-process mock provider, we send
 *       exactly that request to it, and on a match Pact writes the contract JSON to
 *       {@code target/pacts/}. This is fully self-contained: it needs no running provider, so it runs
 *       anywhere {@code mvn test} runs.</li>
 *   <li><b>Provider</b> ({@link #verifyProvider}) - load an existing pact file and replay every
 *       interaction against a real, running provider base URL, asserting the response status (and,
 *       when the contract pins one, the response body) still honor the contract.</li>
 * </ul>
 *
 * The single {@code au.com.dius.pact.consumer:junit5} dependency (which brings the Pact DSL, mock
 * server, and core model transitively) is added to the project's pom only when a test uses a Pact
 * step, so projects that don't need it stay lean. Generated / shipped by L3git.
 */
public class PactClient {

    /**
     * Consumer side. Builds a single-interaction pact, verifies our own request satisfies it against
     * Pact's in-process mock provider, and writes the contract to {@code pactDir} (default
     * {@code target/pacts}).
     */
    public void defineContract(String consumerName, String providerName, String description,
                               String method, String path, String requestBody,
                               int status, String responseBody, String pactDir) {
        String dir = (pactDir == null || pactDir.isEmpty()) ? "target/pacts" : pactDir;
        System.setProperty("pact.rootDir", dir);

        boolean hasReqBody = requestBody != null && !requestBody.isEmpty();
        boolean hasRespBody = responseBody != null && !responseBody.isEmpty();

        var req = consumer(consumerName).hasPactWith(providerName)
                .uponReceiving(description)
                .path(path)
                .method(method.toUpperCase());
        if (hasReqBody) { req = req.headers(Map.of("Content-Type", "application/json")).body(requestBody); }

        var resp = req.willRespondWith().status(status);
        if (hasRespBody) { resp = resp.headers(Map.of("Content-Type", "application/json")).body(responseBody); }
        RequestResponsePact pact = resp.toPact(RequestResponsePact.class);

        MockProviderConfig config = MockProviderConfig.createDefault(PactSpecVersion.V3);
        PactVerificationResult result = runConsumerTest(pact, config,
                (MockServer mockServer, PactTestExecutionContext ctx) -> {
                    sendToMock(mockServer.getUrl(), method, path, hasReqBody ? requestBody : null, status);
                    return null;
                });

        if (!(result instanceof PactVerificationResult.Ok)) {
            throw new AssertionError("Pact consumer contract not satisfied: " + result.toString());
        }
    }

    /** Fire the consumer's own request at the Pact mock server and sanity-check the canned status. */
    private void sendToMock(String baseUrl, String method, String path, String body, int expectedStatus) {
        try {
            HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
            HttpRequest.BodyPublisher pub = (body == null)
                    ? HttpRequest.BodyPublishers.noBody()
                    : HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8);
            HttpRequest.Builder rb = HttpRequest.newBuilder().uri(URI.create(baseUrl + path));
            if (body != null) { rb.header("Content-Type", "application/json"); }
            rb.method(method.toUpperCase(), pub);
            HttpResponse<String> r = client.send(rb.build(), HttpResponse.BodyHandlers.ofString());
            if (r.statusCode() != expectedStatus) {
                throw new AssertionError("Pact mock returned " + r.statusCode() + ", expected " + expectedStatus);
            }
        } catch (AssertionError e) {
            throw e;
        } catch (Exception e) {
            throw new AssertionError("Pact consumer request failed: " + e.getMessage(), e);
        }
    }

    /**
     * Provider side. Load a pact file and replay each interaction against a live provider, asserting
     * the response still honors the contract (status always; response body when the contract pins a
     * non-empty one, checked as containment of the pinned body's significant tokens).
     */
    public void verifyProvider(String baseUrl, String pactFile) {
        File f = new File(pactFile);
        if (!f.isFile()) { throw new AssertionError("Pact file not found: " + pactFile); }
        Pact pact = DefaultPactReader.INSTANCE.loadPact(f);
        List<Interaction> interactions = pact.getInteractions();
        if (interactions.isEmpty()) { throw new AssertionError("Pact file has no interactions: " + pactFile); }

        HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(15)).build();
        String base = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
        int checked = 0;
        for (Interaction it : interactions) {
            if (!(it instanceof RequestResponseInteraction)) { continue; }
            RequestResponseInteraction rr = (RequestResponseInteraction) it;
            String method = rr.getRequest().getMethod();
            String path = rr.getRequest().getPath();
            String reqBody = bodyText(rr.getRequest().getBody());
            int wantStatus = rr.getResponse().getStatus();
            String wantBody = bodyText(rr.getResponse().getBody());
            boolean sendBody = reqBody != null && !reqBody.isEmpty();

            try {
                HttpRequest.BodyPublisher pub = sendBody
                        ? HttpRequest.BodyPublishers.ofString(reqBody, StandardCharsets.UTF_8)
                        : HttpRequest.BodyPublishers.noBody();
                HttpRequest.Builder rb = HttpRequest.newBuilder().uri(URI.create(base + path));
                if (sendBody) { rb.header("Content-Type", "application/json"); }
                rb.method(method.toUpperCase(), pub);
                HttpResponse<String> r = client.send(rb.build(), HttpResponse.BodyHandlers.ofString());

                if (r.statusCode() != wantStatus) {
                    throw new AssertionError("Provider broke contract on " + method + " " + path
                            + ": got status " + r.statusCode() + ", contract expects " + wantStatus);
                }
                if (wantBody != null && !wantBody.isEmpty()) {
                    String got = r.body() == null ? "" : r.body();
                    for (String token : significantTokens(wantBody)) {
                        if (!got.contains(token)) {
                            throw new AssertionError("Provider broke contract on " + method + " " + path
                                    + ": response missing contract field \"" + token + "\"");
                        }
                    }
                }
                checked++;
            } catch (AssertionError e) {
                throw e;
            } catch (Exception e) {
                throw new AssertionError("Provider verification request failed on " + method + " " + path
                        + ": " + e.getMessage(), e);
            }
        }
        if (checked == 0) { throw new AssertionError("Pact file had no request/response interactions: " + pactFile); }
    }

    private static String bodyText(Object body) {
        if (body == null) { return null; }
        try {
            var m = body.getClass().getMethod("valueAsString");
            Object v = m.invoke(body);
            return v == null ? null : v.toString();
        } catch (Exception e) {
            return body.toString();
        }
    }

    /** Pull quoted JSON field names/values from the pinned body so replay tolerates key ordering. */
    private static List<String> significantTokens(String body) {
        java.util.List<String> out = new java.util.ArrayList<>();
        java.util.regex.Matcher mm = java.util.regex.Pattern.compile("\"([^\"]{1,64})\"").matcher(body);
        while (mm.find()) { out.add(mm.group(1)); }
        if (out.isEmpty()) { out.add(body.trim()); }
        return out;
    }
}
