import { test, describe } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import {
  findCollectionFiles,
  parseCollectionFile,
  buildDataMap,
  renderDataMapYaml,
  unifiedDiff,
} from "./emit-data-map.mjs";

// ---- Fixtures ----

const USERS_TS = `
import type { CollectionConfig } from "payload";
export const users: CollectionConfig = {
  slug: "users",
  auth: true,
  custom: {
    retention: { purgeSchedule: "daily", postDeletion: { duration: "P30D", trigger: "after-deletion", action: "hard-delete" } },
  },
  fields: [
    {
      name: "displayName",
      type: "text",
      custom: {
        pii: {
          category: "identification-username",
          purpose: ["service-delivery"],
          exportable: true,
          restrictable: true,
        },
      },
    },
    { name: "role", type: "select" },
  ],
};
`;

const ARTICLES_TS = `
import type { CollectionConfig } from "payload";
export const articles: CollectionConfig = {
  slug: "articles",
  custom: {
    retention: { purgeSchedule: "monthly", postDeletion: { duration: "P90D", trigger: "after-deletion", action: "hard-delete" } },
  },
  fields: [
    { name: "title", type: "text" },
    { name: "content", type: "richText" },
  ],
};
`;

const AUTH_PII_OVERRIDE_TS = `
import type { CollectionConfig } from "payload";
export const members: CollectionConfig = {
  slug: "members",
  auth: true,
  custom: {
    retention: { purgeSchedule: "daily" },
    authPii: {
      email: {
        category: "contact-email",
        purpose: ["account-authentication", "marketing-communications"],
        exportable: false,
        restrictable: true,
      },
      password: null,
    },
  },
  fields: [],
};
`;

// ---- Test helpers ----

function makeRepo(collections) {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "emit-data-map-"));
  for (const [pkgSlug, src] of Object.entries(collections)) {
    const dir = path.join(
      root,
      "packages",
      pkgSlug,
      "src",
      "integrations",
      "cms",
      "collections",
    );
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(path.join(dir, `${pkgSlug}.ts`), src, "utf8");
  }
  return root;
}

function buildMapFromFixtures(fixtures) {
  const root = makeRepo(fixtures);
  const files = findCollectionFiles(root);
  const raw = files.map((f) => parseCollectionFile(f)).filter(Boolean);
  return buildDataMap(raw);
}

// ---- Tests ----

describe("findCollectionFiles", () => {
  test("returns all .ts files in packages/*/integrations/cms/collections/", () => {
    const root = makeRepo({ blog: ARTICLES_TS, auth: USERS_TS });
    const files = findCollectionFiles(root);
    assert.equal(files.length, 2);
    assert.ok(files.every((f) => f.endsWith(".ts")));
    assert.ok(files.some((f) => f.includes("auth")));
    assert.ok(files.some((f) => f.includes("blog")));
  });

  test("returns empty array when packages/ does not exist", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "emit-data-map-empty-"));
    assert.deepEqual(findCollectionFiles(root), []);
  });

  test("excludes .test.ts files", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "emit-data-map-test-"));
    const dir = path.join(
      root,
      "packages",
      "blog",
      "src",
      "integrations",
      "cms",
      "collections",
    );
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(path.join(dir, "articles.ts"), ARTICLES_TS, "utf8");
    fs.writeFileSync(path.join(dir, "articles.test.ts"), "// test", "utf8");
    const files = findCollectionFiles(root);
    assert.equal(files.length, 1);
    assert.ok(!files[0].endsWith(".test.ts"));
  });
});

describe("parseCollectionFile", () => {
  test("extracts slug, auth, fields, and custom from a collection file", () => {
    const root = makeRepo({ auth: USERS_TS });
    const [file] = findCollectionFiles(root);
    const coll = parseCollectionFile(file);
    assert.equal(coll.slug, "users");
    assert.equal(coll.auth, true);
    assert.ok(Array.isArray(coll.fields));
    assert.equal(coll.fields.length, 2);
    assert.equal(coll.fields[0].name, "displayName");
    assert.equal(coll.fields[0].custom.pii.category, "identification-username");
  });

  test("returns null for an unreadable path", () => {
    assert.equal(parseCollectionFile("/nonexistent/path.ts"), null);
  });

  test("extracts a collection without auth or PII", () => {
    const root = makeRepo({ blog: ARTICLES_TS });
    const [file] = findCollectionFiles(root);
    const coll = parseCollectionFile(file);
    assert.equal(coll.slug, "articles");
    assert.equal(coll.auth, undefined);
    assert.equal(coll.fields.length, 2);
    assert.ok(!coll.fields[0].custom);
  });
});

describe("buildDataMap — empty collections", () => {
  test("returns empty map for no collections", () => {
    assert.deepEqual(buildDataMap([]), {});
  });

  test("includes collection with no PII fields as piiFields: []", () => {
    const map = buildMapFromFixtures({ blog: ARTICLES_TS });
    assert.ok("articles" in map);
    assert.deepEqual(map.articles.piiFields, []);
    assert.equal(map.articles.auth, false);
  });
});

describe("buildDataMap — auth defaults applied", () => {
  test("applies PAYLOAD_AUTH_PII_DEFAULTS for auth-enabled collections", () => {
    const map = buildMapFromFixtures({ auth: USERS_TS });
    const emailField = map.users.piiFields.find((f) => f.field === "email");
    assert.ok(emailField, "email field should be present from auth defaults");
    assert.equal(emailField.category, "contact-email");
    assert.equal(emailField.source, "auth-default");
    assert.deepEqual(emailField.purpose, [
      "account-authentication",
      "transactional-notifications",
    ]);
  });

  test("excludes null-valued auth defaults (e.g. password, salt)", () => {
    const map = buildMapFromFixtures({ auth: USERS_TS });
    const nullFields = ["password", "salt", "hash", "resetPasswordToken"];
    for (const name of nullFields) {
      const found = map.users.piiFields.find((f) => f.field === name);
      assert.equal(
        found,
        undefined,
        `${name} should be excluded (null default)`,
      );
    }
  });

  test("does NOT apply auth defaults for non-auth collections", () => {
    const map = buildMapFromFixtures({ blog: ARTICLES_TS });
    assert.deepEqual(map.articles.piiFields, []);
  });

  test("includes field-level PII tags alongside auth defaults", () => {
    const map = buildMapFromFixtures({ auth: USERS_TS });
    const displayName = map.users.piiFields.find(
      (f) => f.field === "displayName",
    );
    const email = map.users.piiFields.find((f) => f.field === "email");
    assert.ok(displayName, "displayName should come from field-tag");
    assert.equal(displayName.source, "field-tag");
    assert.ok(email, "email should come from auth-default");
    assert.equal(email.source, "auth-default");
  });

  test("piiFields are sorted by field name", () => {
    const map = buildMapFromFixtures({ auth: USERS_TS });
    const names = map.users.piiFields.map((f) => f.field);
    assert.deepEqual(names, [...names].sort());
  });
});

describe("buildDataMap — authPii override applied", () => {
  test("custom.authPii overrides PAYLOAD_AUTH_PII_DEFAULTS", () => {
    const map = buildMapFromFixtures({ members: AUTH_PII_OVERRIDE_TS });
    const emailField = map.members.piiFields.find((f) => f.field === "email");
    assert.ok(emailField, "email should be present");
    assert.equal(emailField.source, "auth-override");
    assert.equal(emailField.exportable, false);
    assert.ok(
      emailField.purpose.includes("marketing-communications"),
      "overridden purpose should be used",
    );
  });

  test("authPii null overrides exclude the field", () => {
    const map = buildMapFromFixtures({ members: AUTH_PII_OVERRIDE_TS });
    const pwField = map.members.piiFields.find((f) => f.field === "password");
    assert.equal(
      pwField,
      undefined,
      "password null override should be excluded",
    );
  });
});

describe("renderDataMapYaml", () => {
  test("renders collections in alphabetical order", () => {
    const map = {
      users: { auth: true, piiFields: [], slug: "users" },
      articles: { auth: false, piiFields: [], slug: "articles" },
    };
    const yaml = renderDataMapYaml(map);
    const articlesIdx = yaml.indexOf("  articles:");
    const usersIdx = yaml.indexOf("  users:");
    assert.ok(articlesIdx < usersIdx, "articles should come before users");
  });

  test("renders piiFields: [] for empty fields", () => {
    const map = { articles: { auth: false, piiFields: [], slug: "articles" } };
    const yaml = renderDataMapYaml(map);
    assert.ok(yaml.includes("piiFields: []"));
  });

  test("renders PII field with all required keys", () => {
    const map = {
      users: {
        auth: true,
        slug: "users",
        piiFields: [
          {
            field: "email",
            source: "auth-default",
            category: "contact-email",
            purpose: ["account-authentication"],
            exportable: true,
            restrictable: true,
          },
        ],
      },
    };
    const yaml = renderDataMapYaml(map);
    assert.ok(yaml.includes("field: email"));
    assert.ok(yaml.includes("category: contact-email"));
    assert.ok(yaml.includes("source: auth-default"));
    assert.ok(yaml.includes("exportable: true"));
    assert.ok(yaml.includes("restrictable: true"));
    assert.ok(yaml.includes("- account-authentication"));
  });

  test("rendered YAML includes header comment", () => {
    const yaml = renderDataMapYaml({});
    assert.ok(yaml.startsWith("# compliance/data-map.yml"));
  });

  test("output is deterministic across multiple calls", () => {
    const map = buildMapFromFixtures({ auth: USERS_TS, blog: ARTICLES_TS });
    assert.equal(renderDataMapYaml(map), renderDataMapYaml(map));
  });
});

describe("unifiedDiff", () => {
  test("returns null when strings are equal", () => {
    assert.equal(unifiedDiff("a\nb\n", "a\nb\n", "file.yml"), null);
  });

  test("returns a readable diff when strings differ", () => {
    const diff = unifiedDiff("line1\nold\n", "line1\nnew\n", "file.yml");
    assert.ok(diff !== null);
    assert.ok(diff.includes("--- file.yml"));
    assert.ok(diff.includes("+++ file.yml"));
    assert.ok(diff.includes("- old"));
    assert.ok(diff.includes("+ new"));
  });

  test("diff includes line numbers", () => {
    const diff = unifiedDiff("a\n", "b\n", "x.yml");
    assert.ok(diff.includes("Line 1:"));
  });

  test("handles different lengths (extra lines)", () => {
    const diff = unifiedDiff("a\nb\n", "a\nb\nc\n", "x.yml");
    assert.ok(diff !== null);
    assert.ok(diff.includes("+ c"));
  });
});

describe("--check mode (integration)", () => {
  test("passes when committed file matches generated output", () => {
    const map = buildMapFromFixtures({ blog: ARTICLES_TS });
    const yaml = renderDataMapYaml(map);
    const diff = unifiedDiff(yaml, yaml, "compliance/data-map.yml");
    assert.equal(diff, null, "no diff expected when file matches");
  });

  test("fails when committed file does not match generated output", () => {
    const map = buildMapFromFixtures({ blog: ARTICLES_TS });
    const yaml = renderDataMapYaml(map);
    const stale = "# stale content\ncollections: {}\n";
    const diff = unifiedDiff(stale, yaml, "compliance/data-map.yml");
    assert.ok(diff !== null, "diff expected when file is stale");
    assert.ok(diff.includes("- # stale content"));
  });
});
