package com.example.support;

import io.restassured.RestAssured;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;

import org.openqa.selenium.Cookie;
import org.openqa.selenium.WebDriver;

import java.io.File;
import java.io.FileInputStream;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

/**
 * Shared API test context (L3git). Holds variables extracted from responses, accumulated
 * request headers, and the last response, so API steps chain together in one .feature.
 *
 * REST Assured is the single HTTP engine (REST / GraphQL / raw SOAP). The base URI comes from
 * -Dapi.baseUri=... or the API_BASE_URI env var; leave it blank to use absolute URLs per step.
 *
 * This copy (the UI framework) also carries the UI + API COOKIE BRIDGE: {@link #useCookies} copies
 * a live browser's cookies into the API calls, so a scenario can log in through Selenium and then
 * hit the backend as that authenticated user. Generated by L3git - regenerate rather than editing.
 */
public class ApiContext {

    /** Values saved from responses, referenced later as ${name} in any URL / header / body. */
    public final Map<String, String> vars = new HashMap<>();

    /** Headers applied to following requests (persist until changed, e.g. an auth token). */
    public final Map<String, String> headers = new LinkedHashMap<>();

    /** Cookies carried into API requests (e.g. copied from the browser session). */
    public final Map<String, String> cookies = new HashMap<>();

    /** The most recent response - what the assertion / extract steps read. */
    public Response last;

    /** Optional base URI (blank = each step uses an absolute URL). */
    public String baseUri = System.getProperty("api.baseUri",
            System.getenv().getOrDefault("API_BASE_URI", ""));

    public ApiContext() {
        loadSecrets();
    }

    /**
     * Seed ${vars} with tokens / keys so they resolve at runtime WITHOUT being written into the
     * committed test code. Sources, in order (later wins):
     *   1) a local properties file (gitignored) - default "secrets.properties", or -Dl3git.secrets=PATH
     *      / the L3GIT_SECRETS env var. Keep real token values here; it never goes into git.
     *   2) environment variables named L3GIT_SECRET_&lt;NAME&gt;  ->  ${&lt;NAME&gt;}  (for CI secrets).
     *   3) an env var whose name already matches a key from the file (lets CI override a file value).
     */
    private void loadSecrets() {
        String path = System.getProperty("l3git.secrets",
                System.getenv().getOrDefault("L3GIT_SECRETS", "secrets.properties"));
        try {
            File f = new File(path);
            if (f.isFile()) {
                Properties p = new Properties();
                try (FileInputStream in = new FileInputStream(f)) {
                    p.load(in);
                }
                for (String k : p.stringPropertyNames()) {
                    vars.put(k, p.getProperty(k));
                }
            }
        } catch (Exception ignore) {
            // no/unreadable secrets file - fine, values may come from env or be set at runtime
        }
        for (Map.Entry<String, String> e : System.getenv().entrySet()) {
            String k = e.getKey();
            if (k.startsWith("L3GIT_SECRET_")) {
                vars.put(k.substring("L3GIT_SECRET_".length()), e.getValue());
            } else if (vars.containsKey(k)) {
                vars.put(k, e.getValue());
            }
        }
    }

    /** Copy the live browser's cookies into the API context (UI -> API auth bridge). */
    public void useCookies(WebDriver driver) {
        if (driver == null) {
            return;
        }
        for (Cookie c : driver.manage().getCookies()) {
            cookies.put(c.getName(), c.getValue());
        }
    }

    /** Replace every ${name} placeholder with a saved variable (blank if unknown). */
    public String resolve(String s) {
        if (s == null) {
            return null;
        }
        String out = s;
        for (Map.Entry<String, String> e : vars.entrySet()) {
            out = out.replace("${" + e.getKey() + "}", e.getValue() == null ? "" : e.getValue());
        }
        return out;
    }

    public void setHeader(String name, String value) {
        headers.put(resolve(name), resolve(value));
    }

    private RequestSpecification base() {
        RequestSpecification req = RestAssured.given();
        if (baseUri != null && !baseUri.isEmpty()) {
            req = req.baseUri(baseUri);
        }
        for (Map.Entry<String, String> e : headers.entrySet()) {
            req = req.header(e.getKey(), e.getValue());
        }
        for (Map.Entry<String, String> e : cookies.entrySet()) {
            req = req.cookie(e.getKey(), e.getValue());
        }
        return req;
    }

    /** Send a request and store the response. Body may be null/empty for GET/DELETE. */
    public Response send(String method, String path, String body) {
        RequestSpecification req = base();
        if (body != null && !body.isEmpty()) {
            if (!headers.containsKey("Content-Type")) {
                req = req.contentType("application/json");
            }
            req = req.body(resolve(body));
        }
        last = req.request(method.toUpperCase(), resolve(path));
        return last;
    }

    /**
     * Send a request WITHOUT storing it as {@code last}. Safe to call concurrently (load testing):
     * it only reads the shared headers / vars / cookies, which the load run never mutates.
     */
    public Response fire(String method, String path, String body) {
        RequestSpecification req = base();
        if (body != null && !body.isEmpty()) {
            if (!headers.containsKey("Content-Type")) {
                req = req.contentType("application/json");
            }
            req = req.body(resolve(body));
        }
        return req.request(method.toUpperCase(), resolve(path));
    }

    public int status() {
        return last.getStatusCode();
    }

    /** A response header value (null if absent). */
    public String header(String name) {
        return last == null ? null : last.getHeader(name);
    }

    /** The raw response body as text. */
    public String body() {
        return last == null ? null : last.asString();
    }

    /** The last response's round-trip time in milliseconds (-1 if no request yet). */
    public long timeMs() {
        return last == null ? -1 : last.time();
    }

    /** Read a value by GPath / JSON-path. A leading "$." (JSONPath style) is accepted and stripped. */
    public String json(String jsonPath) {
        String p = jsonPath == null ? "" : jsonPath.replaceFirst("^\\$\\.?", "");
        return last.jsonPath().getString(p);
    }

    public void save(String name, String value) {
        vars.put(name, value);
    }

    /**
     * Run a LOCAL file/script and return its trimmed stdout - e.g. a login script that mints a
     * token, so a later request can send it. The interpreter is inferred from the extension
     * (.ps1/.py/.sh/.bat/.cmd); anything else runs directly (.exe / shebang). Throws on a non-zero
     * exit. The file must exist on whatever machine runs the test - ship it with the project (attach
     * it so it lands in the project root) or provide it on the build agent.
     */
    public String runLocal(String file, String args) {
        try {
            java.util.List<String> cmd = buildLocalCmd(file, args);
            Process p = new ProcessBuilder(cmd).start();
            byte[] o = readAll(p.getInputStream());
            byte[] er = readAll(p.getErrorStream());
            int code = p.waitFor();
            String out = new String(o, java.nio.charset.StandardCharsets.UTF_8);
            if (code != 0) {
                String err = new String(er, java.nio.charset.StandardCharsets.UTF_8);
                throw new RuntimeException("run file '" + file + "' exited " + code + ": " + err.trim());
            }
            return out.trim();
        } catch (RuntimeException re) {
            throw re;
        } catch (Exception e) {
            throw new RuntimeException("run file '" + file + "' failed: " + e.getMessage(), e);
        }
    }

    private java.util.List<String> buildLocalCmd(String file, String args) {
        String lower = file == null ? "" : file.toLowerCase();
        boolean win = System.getProperty("os.name", "").toLowerCase().contains("win");
        java.util.List<String> c = new java.util.ArrayList<>();
        if (lower.endsWith(".ps1")) { c.add(win ? "powershell.exe" : "pwsh"); c.add("-NoProfile"); c.add("-ExecutionPolicy"); c.add("Bypass"); c.add("-File"); c.add(file); }
        else if (lower.endsWith(".py")) { c.add("python"); c.add(file); }
        else if (lower.endsWith(".sh")) { c.add("bash"); c.add(file); }
        else if (lower.endsWith(".bat") || lower.endsWith(".cmd")) { c.add("cmd.exe"); c.add("/c"); c.add(file); }
        else { c.add(file); }
        if (args != null && !args.trim().isEmpty()) { for (String a : args.trim().split("\\s+")) { c.add(a); } }
        return c;
    }

    private static byte[] readAll(java.io.InputStream in) throws java.io.IOException {
        java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
        byte[] buf = new byte[4096]; int n;
        while ((n = in.read(buf)) != -1) { bos.write(buf, 0, n); }
        return bos.toByteArray();
    }

    /**
     * OAuth2 token fetch (client_credentials or password grant). POSTs the form to the token URL,
     * reads access_token, and sets the Bearer Authorization header for all following requests.
     * Pass secrets via -Dapi.oauth.* on the command line to keep them out of source in CI.
     */
    public void oauth2(String grant, String tokenUrl, String clientId, String clientSecret,
                       String scope, String username, String password) {
        RequestSpecification req = RestAssured.given()
                .contentType("application/x-www-form-urlencoded")
                .formParam("grant_type", grant)
                .formParam("client_id", clientId)
                .formParam("client_secret", clientSecret);
        if (scope != null && !scope.isEmpty()) {
            req = req.formParam("scope", scope);
        }
        if ("password".equalsIgnoreCase(grant)) {
            req = req.formParam("username", username == null ? "" : username)
                     .formParam("password", password == null ? "" : password);
        }
        last = req.post(resolve(tokenUrl));
        String token = last.jsonPath().getString("access_token");
        if (token != null) {
            headers.put("Authorization", "Bearer " + token);
        }
    }

    /** SOAP: POST a raw XML envelope as text/xml (still through REST Assured). */
    public Response sendSoap(String path, String body) {
        RequestSpecification req = RestAssured.given().contentType("text/xml; charset=utf-8");
        for (Map.Entry<String, String> e : headers.entrySet()) {
            req = req.header(e.getKey(), e.getValue());
        }
        for (Map.Entry<String, String> e : cookies.entrySet()) {
            req = req.cookie(e.getKey(), e.getValue());
        }
        last = req.body(resolve(body)).post(resolve(path));
        return last;
    }

    /**
     * Read an element's text from the last (XML) response by local name - namespace-agnostic, so
     * a path like "//status" or "status" matches &lt;ns:status&gt; regardless of its prefix.
     */
    public String xml(String path) {
        try {
            String leaf = path.replaceAll(".*/", "").replaceAll(".*:", "").replaceAll("[^A-Za-z0-9_.-]", "");
            javax.xml.parsers.DocumentBuilderFactory f = javax.xml.parsers.DocumentBuilderFactory.newInstance();
            f.setNamespaceAware(true);
            org.w3c.dom.Document doc = f.newDocumentBuilder()
                    .parse(new org.xml.sax.InputSource(new java.io.StringReader(last.asString())));
            javax.xml.xpath.XPath xp = javax.xml.xpath.XPathFactory.newInstance().newXPath();
            String v = xp.evaluate("//*[local-name()='" + leaf + "']", doc);
            return (v == null || v.isEmpty()) ? null : v;
        } catch (Exception e) {
            return null;
        }
    }
}
