import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import { TemplateNotFoundError, TemplateVarValidationFailedError } from "../lib/errors.generated";
import { baseTemplate, baseEmailTemplate } from "../testing/fixtures";
import { run } from "./renderNotificationTemplate";

describe("renderNotificationTemplate", () => {
  it("renders subject and body with placeholders substituted when an exact-locale template exists and payloadVars satisfies the schema", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "en-US",
      payloadVars: { poNumber: "PO-001" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.subject).toBe("PO PO-001 issued");
      expect(result.value.body).toBe("Purchase order PO-001 has been issued.");
    }
  });

  it("falls back to the default-locale template when the exact-locale row is missing", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], [{ ...baseTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "ja-JP",
      payloadVars: { poNumber: "PO-002" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.subject).toBe("PO PO-002 issued");
    }
  });

  it("prefers the exact-locale template when both the exact and default-locale rows exist", async () => {
    const exactLocale = {
      ...baseTemplate,
      locale: "ja-JP",
      subject: "発注 {{poNumber}} 発行",
      body: "発注書 {{poNumber}} が発行されました。",
    };
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([exactLocale]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "ja-JP",
      payloadVars: { poNumber: "PO-003" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.subject).toBe("発注 PO-003 発行");
    }
  });

  it("fails with TEMPLATE_NOT_FOUND when neither the exact-locale nor the default-locale template exists", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], []);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "ja-JP",
      payloadVars: { poNumber: "PO-004" },
    });

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(TemplateNotFoundError);
    }
  });

  it("fails with TEMPLATE_VAR_VALIDATION_FAILED when a required declared variable is missing", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "en-US",
      payloadVars: {},
    });

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(TemplateVarValidationFailedError);
    }
  });

  it("fails with TEMPLATE_VAR_VALIDATION_FAILED when a declared variable receives a wrong-type value", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "en-US",
      payloadVars: { poNumber: 123 },
    });

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(TemplateVarValidationFailedError);
    }
  });

  it("ignores variables present in payloadVars that are not declared by the schema", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "en-US",
      payloadVars: { poNumber: "PO-005", extra: "ignored" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.subject).toBe("PO PO-005 issued");
    }
  });

  it("renders an optional declared variable that is not supplied as empty string", async () => {
    const optionalSchema = {
      ...baseTemplate,
      subject: "Hello {{recipientName}}",
      body: "Hi {{recipientName}}, PO {{poNumber}} is ready.",
      variableSchema:
        '{"variables":[{"name":"poNumber","required":true,"type":"string"},{"name":"recipientName","required":false,"type":"string"}]}',
    };
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([optionalSchema]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "en-US",
      payloadVars: { poNumber: "PO-006" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.subject).toBe("Hello ");
      expect(result.value.body).toBe("Hi , PO PO-006 is ready.");
    }
  });

  it("returns htmlBody when the template declares one and applies substitution to it", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseEmailTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-2",
      locale: "en-US",
      payloadVars: { poNumber: "PO-007" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.htmlBody).toBe("<p>Purchase order PO-007 has been issued.</p>");
    }
  });

  it("returns htmlBody as undefined / absent when the template does not declare one", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "en-US",
      payloadVars: { poNumber: "PO-008" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.htmlBody).toBeUndefined();
    }
  });

  it("HTML-escapes user-supplied variable values in htmlBody so embedded markup cannot break the document", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseEmailTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-2",
      locale: "en-US",
      payloadVars: { poNumber: "<script>alert('x')</script>" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.htmlBody).toContain("&lt;script&gt;");
      expect(result.value.htmlBody).not.toContain("<script>");
    }
  });

  it("inserts variable values verbatim in the plain-text body without HTML-escaping", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "en-US",
      payloadVars: { poNumber: "<raw>" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.body).toBe("Purchase order <raw> has been issued.");
    }
  });

  it("substitutes the same variable at every position in subject, body, and htmlBody", async () => {
    const repeatedTemplate = {
      ...baseEmailTemplate,
      subject: "{{poNumber}} :: {{poNumber}}",
      body: "[{{poNumber}}] body [{{poNumber}}]",
      htmlBody: "<p>{{poNumber}} / {{poNumber}}</p>",
    };
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([repeatedTemplate]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-2",
      locale: "en-US",
      payloadVars: { poNumber: "X" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.subject).toBe("X :: X");
      expect(result.value.body).toBe("[X] body [X]");
      expect(result.value.htmlBody).toBe("<p>X / X</p>");
    }
  });

  it("treats constructs that look like loops or conditionals (e.g. {{#if}}) as literal text", async () => {
    const literalTemplate = {
      ...baseTemplate,
      subject: "PO {{poNumber}} {{#if x}}cond{{/if}}",
      body: "Body {{#each items}} loop {{/each}} {{poNumber}}",
    };
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([literalTemplate]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "en-US",
      payloadVars: { poNumber: "PO-009" },
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.subject).toBe("PO PO-009 {{#if x}}cond{{/if}}");
      expect(result.value.body).toBe("Body {{#each items}} loop {{/each}} PO-009");
    }
  });

  it("does not persist any state; subsequent template edits are reflected on the next render call", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseTemplate }]);

    const result = await run(mock.db, {
      eventType: "PO_ISSUED",
      channelId: "channel-1",
      locale: "en-US",
      payloadVars: { poNumber: "PO-010" },
    });

    expect(result.ok).toBe(true);
    expect(mock.inserts.length).toBe(0);
    expect(mock.updates.length).toBe(0);
    expect(mock.deletes.length).toBe(0);
  });
});
