import { test, expect } from "bun:test";
import {
  Window,
  type Event as HappyDOMEvent,
  type HTMLFormElement as HappyDOMHTMLFormElement,
  type HTMLInputElement as HappyDOMHTMLInputElement,
  type HTMLElement as HappyDOMHTMLElement,
  type IEventInit,
} from "happy-dom";
import { v, invalid, parseBody, checkField, parseEnv, VALIDATE_RUNTIME } from "../src/index.ts";

type HappyDOMEventConstructor = new (type: string, init?: IEventInit) => HappyDOMEvent;

function windowEvent(win: Window, type: string, init?: IEventInit): HappyDOMEvent {
  const EventConstructor = (win as unknown as { Event: HappyDOMEventConstructor }).Event;
  return new EventConstructor(type, init);
}

const login = v.object({
  email: v.string().email(),
  password: v.string().min(8, "too short"),
  age: v.number().min(18).optional(),
});

test("parse coerces valid input and reports no errors", () => {
  const r = login.parse({ email: "a@b.com", password: "secretpw", age: "25" });
  expect(r.ok).toBe(true);
  expect(r.value).toEqual({ email: "a@b.com", password: "secretpw", age: 25 });
  expect(r.errors).toEqual({});
});

test("parse reports per-field errors with custom messages", () => {
  const r = login.parse({ email: "nope", password: "x" });
  expect(r.ok).toBe(false);
  expect(r.errors.email).toBe("Must be a valid email");
  expect(r.errors.password).toBe("too short");
});

test("optional fields may be omitted", () => {
  const r = login.parse({ email: "a@b.com", password: "longenough" });
  expect(r.ok).toBe(true);
  expect(r.value.age).toBeUndefined();
});

test("required (non-optional) empty fields fail", () => {
  const r = login.parse({});
  expect(r.errors.email).toBe("Required");
  expect(r.errors.password).toBe("Required");
});

test("required(message) customizes empty-field errors and overrides optional", () => {
  const schema = v.object({
    email: v.string().required("Enter your email or username"),
    password: v.string().optional().required("Enter your password"),
    accepted: v.boolean().required("Accept the terms to continue"),
  });
  const result = schema.parse({ accepted: false });

  expect(result.errors).toEqual({
    email: "Enter your email or username",
    password: "Enter your password",
    accepted: "Accept the terms to continue",
  });
  expect(schema.describe().fields.email.requiredMessage).toBe("Enter your email or username");
});

test("browser validation displays the serialized custom required message", () => {
  const schema = v.object({
    email: v.string().required("Enter your email or username"),
  });
  const win = new Window() as unknown as Window & Record<string, unknown>;
  win.document.body.innerHTML = `
    <form data-schema="login">
      <input name="email">
      <span data-error="email"></span>
    </form>`;
  win.__wireSchemas = { login: schema.describe() };
  (globalThis as Record<string, unknown>).window = win;
  (globalThis as Record<string, unknown>).document = win.document;

  try {
    (0, eval)(VALIDATE_RUNTIME);
    const runtime = win.__wireValidate as { init(root: Document): void };
    runtime.init(win.document as unknown as Document);
    const form = win.document.querySelector("form")!;
    win.document.querySelector("input")!.dispatchEvent(windowEvent(win, "blur", { bubbles: true }));

    expect(form.noValidate).toBe(true);
    expect(form.hasAttribute("novalidate")).toBe(true);
    expect(win.document.querySelector("[data-error=email]")!.textContent).toBe(
      "Enter your email or username",
    );
  } finally {
    delete (globalThis as Record<string, unknown>).window;
    delete (globalThis as Record<string, unknown>).document;
  }
});

test("describe emits a JSON descriptor", () => {
  const d = login.describe();
  expect(d.type).toBe("object");
  expect(d.fields.email.rules).toContainEqual({ kind: "email" });
  expect(d.fields.age.optional).toBe(true);
});

test("checkField coerces and applies rules", () => {
  expect(checkField({ type: "number", rules: [{ kind: "min", n: 18 }] }, "5").error).toBe(
    "Must be at least 18",
  );
  expect(checkField({ type: "number", rules: [{ kind: "min", n: 18 }] }, "20").value).toBe(20);
  expect(checkField({ type: "string", optional: true, rules: [] }, "").error).toBeNull();
  expect(
    checkField({ type: "string", requiredMessage: "Email is required", rules: [] }, "").error,
  ).toBe("Email is required");
});

test("invalid() returns a 400 with errors", async () => {
  const res = invalid({ email: "bad" });
  expect(res.status).toBe(400);
  expect(await res.json()).toEqual({ ok: false, errors: { email: "bad" } });
});

test("parseBody reads JSON and form-encoded bodies", async () => {
  const jsonReq = new Request("http://x", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ email: "a@b.com", password: "longenough" }),
  });
  const j = await parseBody(login, jsonReq);
  expect(j.ok).toBe(true);

  const formReq = new Request("http://x", {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: "email=a@b.com&password=longenough",
  });
  const f = await parseBody(login, formReq);
  expect(f.ok).toBe(true);
});

test("new string rules: url, uuid, date, length, oneOf, trim", () => {
  const s = v.object({
    site: v.string().url(),
    id: v.string().uuid(),
    when: v.string().date(),
    code: v.string().length(4),
    role: v.string().oneOf(["admin", "user"]),
    name: v.string().trim().min(2),
  });
  const ok = s.parse({
    site: "https://x.com/a",
    id: "123e4567-e89b-12d3-a456-426614174000",
    when: "2026-01-01",
    code: "ABCD",
    role: "admin",
    name: "  Ada  ",
  });
  expect(ok.ok).toBe(true);
  expect(ok.value.name).toBe("Ada"); // trimmed

  const bad = s.parse({
    site: "not-a-url",
    id: "nope",
    when: "not-a-date",
    code: "AB",
    role: "root",
    name: " a ",
  });
  expect(bad.ok).toBe(false);
  expect(Object.keys(bad.errors).sort()).toEqual(["code", "id", "name", "role", "site", "when"]);
});

test("number rules: positive, oneOf", () => {
  const s = v.object({ qty: v.number().positive(), size: v.number().oneOf([1, 2, 3]) });
  expect(s.parse({ qty: "5", size: "2" }).ok).toBe(true);
  expect(s.parse({ qty: "-1", size: "9" }).ok).toBe(false);
});

test("default() fills absent fields; refine() runs server-side", () => {
  const s = v.object({
    role: v.string().default("user"),
    even: v.number().refine((n) => (n as number) % 2 === 0, "Must be even"),
  });
  const r = s.parse({ even: 4 });
  expect(r.ok).toBe(true);
  expect(r.value.role).toBe("user"); // default applied
  const bad = s.parse({ even: 3 });
  expect(bad.ok).toBe(false);
  expect(bad.errors.even).toBe("Must be even");
});

const envSchema = v.object({
  DATABASE_URL: v.string().min(1),
  PORT: v.number(),
  DEBUG: v.boolean().optional(),
});

test("parseEnv coerces and returns typed values", () => {
  const env = parseEnv<{ DATABASE_URL: string; PORT: number; DEBUG?: boolean }>(envSchema, {
    DATABASE_URL: "sqlite://dev.db",
    PORT: "3000",
    DEBUG: "true",
  });
  expect(env.DATABASE_URL).toBe("sqlite://dev.db");
  expect(env.PORT).toBe(3000);
  expect(env.DEBUG).toBe(true);
});

test("parseEnv throws one readable error listing every missing/invalid var", () => {
  let message = "";
  try {
    parseEnv(envSchema, { PORT: "not-a-number" });
  } catch (err) {
    message = (err as Error).message;
  }
  expect(message).toContain("Invalid environment variables");
  expect(message).toContain("DATABASE_URL");
  expect(message).toContain("PORT");
});

test("parseEnv defaults to reading the ambient environment", () => {
  process.env.WIRE_TEST_ENV_VAR = "present";
  const env = parseEnv<{ WIRE_TEST_ENV_VAR: string }>(
    v.object({ WIRE_TEST_ENV_VAR: v.string().min(1) }),
  );
  expect(env.WIRE_TEST_ENV_VAR).toBe("present");
  delete process.env.WIRE_TEST_ENV_VAR;
});

test("object schemas can be extended without mutating package defaults", () => {
  const base = v.object({ identifier: v.string().required("Enter an identifier") });
  const extended = base.extend({
    identifier: v.string().email("Use an email address"),
    remember: v.boolean().optional(),
  });

  expect(base.parse({ identifier: "username" }).ok).toBe(true);
  expect(extended.parse({ identifier: "username" }).errors.identifier).toBe("Use an email address");
  expect(extended.parse({ identifier: "person@example.com" }).ok).toBe(true);
});

test("unknown schemas preserve structured request payloads", () => {
  const schema = v.object({ response: v.unknown().required("Response is required") });
  const payload = { id: "credential", nested: { ok: true } };
  const result = schema.parse({ response: payload });
  expect(result.ok).toBe(true);
  expect(result.value.response).toEqual(payload);
  expect(schema.parse({}).errors.response).toBe("Response is required");
});

test("validation can bind after a package registers a missing schema", () => {
  const win = new Window() as unknown as Window & Record<string, unknown>;
  win.document.body.innerHTML = `
    <form data-schema="late-schema">
      <input name="identifier">
      <span data-error="identifier"></span>
    </form>`;
  win.__wireSchemas = {};
  (globalThis as Record<string, unknown>).window = win;
  (globalThis as Record<string, unknown>).document = win.document;

  try {
    (0, eval)(VALIDATE_RUNTIME);
    const runtime = win.__wireValidate as { init(root: Document): void };
    runtime.init(win.document as unknown as Document);
    win.__wireSchemas = {
      "late-schema": v
        .object({ identifier: v.string().required("Enter an identifier") })
        .describe(),
    };
    runtime.init(win.document as unknown as Document);
    win.document
      .querySelector("form")!
      .dispatchEvent(windowEvent(win, "submit", { bubbles: true, cancelable: true }));
    expect(win.document.querySelector("[data-error=identifier]")!.textContent).toBe(
      "Enter an identifier",
    );
  } finally {
    delete (globalThis as Record<string, unknown>).window;
    delete (globalThis as Record<string, unknown>).document;
  }
});

test("schema-backed forms validate on input, change, and blur and synchronize field state", () => {
  const win = new Window() as unknown as Window & Record<string, unknown>;
  win.document.body.innerHTML = `
    <form data-schema="profile">
      <div class="wire-next--field" data-invalid="false">
        <input name="email">
        <span data-error="email"></span>
      </div>
    </form>`;
  win.__wireSchemas = {
    profile: v
      .object({ email: v.string().required("Enter an email").email("Use a valid email") })
      .describe(),
  };
  (globalThis as Record<string, unknown>).window = win;
  (globalThis as Record<string, unknown>).document = win.document;

  try {
    (0, eval)(VALIDATE_RUNTIME);
    const input = win.document.querySelector("input") as HappyDOMHTMLInputElement;
    const field = win.document.querySelector(".wire-next--field") as HappyDOMHTMLElement;
    const error = win.document.querySelector("[data-error=email]") as HappyDOMHTMLElement;

    input.dispatchEvent(windowEvent(win, "input", { bubbles: true }));
    expect(error.textContent).toBe("Enter an email");
    expect(input.getAttribute("aria-invalid")).toBe("true");
    expect(field.dataset.invalid).toBe("true");

    input.value = "person@example.com";
    input.dispatchEvent(windowEvent(win, "change", { bubbles: true }));
    expect(error.textContent).toBe("");
    expect(input.hasAttribute("aria-invalid")).toBe(false);
    expect(field.dataset.invalid).toBe("false");
  } finally {
    delete (globalThis as Record<string, unknown>).window;
    delete (globalThis as Record<string, unknown>).document;
  }
});

test("submit guards run only after schema fields are valid", () => {
  const win = new Window() as unknown as Window & Record<string, unknown>;
  win.document.body.innerHTML = `
    <form data-schema="guarded">
      <input name="email">
      <span data-error="email"></span>
    </form>`;
  win.__wireSchemas = {
    guarded: v.object({ email: v.string().required("Enter an email") }).describe(),
  };
  (globalThis as Record<string, unknown>).window = win;
  (globalThis as Record<string, unknown>).document = win.document;

  try {
    (0, eval)(VALIDATE_RUNTIME);
    const form = win.document.querySelector("form")! as HappyDOMHTMLFormElement & {
      __wireSubmitGuards?: Array<() => boolean>;
    };
    let guardCalls = 0;
    form.__wireSubmitGuards = [
      () => {
        guardCalls += 1;
        return false;
      },
    ];

    form.dispatchEvent(windowEvent(win, "submit", { bubbles: true, cancelable: true }));
    expect(guardCalls).toBe(0);
    expect(win.document.querySelector("[data-error=email]")!.textContent).toBe("Enter an email");

    (form.elements.namedItem("email") as HappyDOMHTMLInputElement).value = "person@example.com";
    form.dispatchEvent(windowEvent(win, "submit", { bubbles: true, cancelable: true }));
    expect(guardCalls).toBe(1);
  } finally {
    delete (globalThis as Record<string, unknown>).window;
    delete (globalThis as Record<string, unknown>).document;
  }
});
