import type { QueryContext } from "@tailor-platform/erp-kit/core";
import { InsufficientPermissionError } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  ValidationErrorError,
  InvalidScopeError,
  UnauthorizedError,
} from "../lib/errors.generated";
import { baseAuditEntry, globalAuditEntry } from "../testing/fixtures";
import { run } from "./getAuditSummary";

const ctx: QueryContext = {
  actorId: "test-actor",
  permissions: ["audit:audit:viewAuditSummary"],
} as const;

const updateEntry = {
  ...baseAuditEntry,
  id: "entry-3",
  entityType: "SalesOrder",
  operationType: "UPDATE",
  actorType: "USER",
  timestamp: new Date("2024-01-15T14:00:00.000Z"),
};

const createEntry = {
  ...baseAuditEntry,
  id: "entry-4",
  entityType: "PurchaseOrder",
  operationType: "CREATE",
  actorType: "SYSTEM",
  timestamp: new Date("2024-02-10T09:00:00.000Z"),
};

describe("getAuditSummary", () => {
  it("returns entry counts grouped by entityType", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry, createEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(2);
      const salesOrder = result.value.summary.find((s) => s.entityType === "SalesOrder");
      const purchaseOrder = result.value.summary.find((s) => s.entityType === "PurchaseOrder");
      expect(salesOrder?.count).toBe(1);
      expect(purchaseOrder?.count).toBe(1);
    }
  });

  it("returns entry counts grouped by operationType", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry, createEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { operationType: true },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(2);
      const update = result.value.summary.find((s) => s.operationType === "UPDATE");
      const create = result.value.summary.find((s) => s.operationType === "CREATE");
      expect(update?.count).toBe(1);
      expect(create?.count).toBe(1);
    }
  });

  it("returns entry counts grouped by actorType", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry, createEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { actorType: true },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(2);
      const user = result.value.summary.find((s) => s.actorType === "USER");
      const system = result.value.summary.find((s) => s.actorType === "SYSTEM");
      expect(user?.count).toBe(1);
      expect(system?.count).toBe(1);
    }
  });

  it("returns entry counts grouped by time period (DAY)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry, createEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { timePeriod: "DAY" },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(2);
      const jan15 = result.value.summary.find((s) => s.timePeriod === "2024-01-15");
      const feb10 = result.value.summary.find((s) => s.timePeriod === "2024-02-10");
      expect(jan15?.count).toBe(1);
      expect(feb10?.count).toBe(1);
    }
  });

  it("returns entry counts grouped by time period (WEEK)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    // Two entries in the same week
    const entry1 = { ...updateEntry, timestamp: new Date("2024-01-15T10:00:00.000Z") };
    const entry2 = {
      ...updateEntry,
      id: "entry-5",
      timestamp: new Date("2024-01-16T10:00:00.000Z"),
    };
    mock.enqueueResults([entry1, entry2]);

    const result = await run(
      mock.db,
      {
        groupBy: { timePeriod: "WEEK" },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
      expect(result.value.summary[0].count).toBe(2);
    }
  });

  it("returns entry counts grouped by time period (MONTH)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry, createEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { timePeriod: "MONTH" },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(2);
      const jan = result.value.summary.find((s) => s.timePeriod === "2024-01");
      const feb = result.value.summary.find((s) => s.timePeriod === "2024-02");
      expect(jan?.count).toBe(1);
      expect(feb?.count).toBe(1);
    }
  });

  it("combines multiple grouping dimensions", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry, createEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true, operationType: true },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(2);
      const salesUpdate = result.value.summary.find(
        (s) => s.entityType === "SalesOrder" && s.operationType === "UPDATE",
      );
      expect(salesUpdate?.count).toBe(1);
    }
  });

  it("filters by entityType before aggregation", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { operationType: true },
        filters: { entityType: "SalesOrder" },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
      expect(result.value.summary[0].operationType).toBe("UPDATE");
    }
  });

  it("filters by operationType before aggregation", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        filters: { operationType: "UPDATE" },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
      expect(result.value.summary[0].entityType).toBe("SalesOrder");
    }
  });

  it("filters by date range before aggregation", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        filters: {
          dateFrom: new Date("2024-01-01"),
          dateTo: new Date("2024-01-31"),
        },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
    }
  });

  it("filters by companyId returning only company-scoped counts", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([updateEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        filters: { companyId: "company-1" },
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
    }
  });

  it("returns global entry counts when companyId is omitted", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([globalAuditEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
      expect(result.value.summary[0].entityType).toBe("User");
    }
  });

  it("returns combined company and global counts when scope is ALL", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAuditEntry, globalAuditEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(2);
    }
  });

  it("restricts combined results to scopes the caller is authorized for", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAuditEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
    }
  });

  it("returns only company counts in combined scope when caller lacks global permission", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAuditEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
    }
  });

  it("returns only global counts in combined scope when caller lacks company permission", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([globalAuditEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
    }
  });

  it("filters combined scope results by companyId when both scope=ALL and companyId are provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    // scope=ALL + companyId returns company entries + global entries (combined)
    mock.enqueueResults([baseAuditEntry, globalAuditEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        filters: { companyId: "company-1" },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(2);
    }
  });

  it("rejects scope=COMPANY when companyId is not provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "COMPANY",
      },
      ctx,
    );

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

  it("rejects invalid scope value", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "INVALID",
      },
      ctx,
    );

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

  it("returns zero counts when no entries match filters", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        filters: { entityType: "NonExistent" },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(0);
    }
  });

  it("rejects when no grouping dimension is specified", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await run(
      mock.db,
      {
        groupBy: {},
      },
      ctx,
    );

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

  it("rejects when caller lacks viewAuditSummary permission", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const noPermsCtx: QueryContext = { actorId: "test-actor", permissions: [] };

    const result = await run(mock.db, { groupBy: { entityType: true }, scope: "ALL" }, noPermsCtx);

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

  it("rejects when caller lacks permission for requested company scope", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAuditEntry]);
    const scopedCtx: QueryContext = {
      actorId: "test-actor",
      permissions: ["audit:audit:viewAuditSummary"],
      companyId: "company-1",
    };

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        filters: { companyId: "company-2" },
      },
      scopedCtx,
    );

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

  it("restricts company-scoped caller to their company even when scope=GLOBAL", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);
    const scopedCtx: QueryContext = {
      actorId: "test-actor",
      permissions: ["audit:audit:viewAuditSummary"],
      companyId: "company-1",
    };

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "GLOBAL",
      },
      scopedCtx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(0);
    }
  });

  it("restricts company-scoped caller to their company even when scope=ALL", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAuditEntry]);
    const scopedCtx: QueryContext = {
      actorId: "test-actor",
      permissions: ["audit:audit:viewAuditSummary"],
      companyId: "company-1",
    };

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "ALL",
      },
      scopedCtx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
      expect(result.value.summary[0].entityType).toBe("SalesOrder");
    }
  });

  it("restricts global-scoped caller to global entries even when scope=COMPANY", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const globalCtx: QueryContext = {
      actorId: "test-actor",
      permissions: ["audit:audit:viewAuditSummary"],
      companyId: null,
    };

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        filters: { companyId: "company-1" },
        scope: "COMPANY",
      },
      globalCtx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(0);
    }
  });

  it("restricts global-scoped caller to global entries even when scope=ALL", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([globalAuditEntry]);
    const globalCtx: QueryContext = {
      actorId: "test-actor",
      permissions: ["audit:audit:viewAuditSummary"],
      companyId: null,
    };

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "ALL",
      },
      globalCtx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.summary).toHaveLength(1);
      expect(result.value.summary[0].entityType).toBe("User");
    }
  });

  it("does not expose field-level change details in response", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAuditEntry]);

    const result = await run(
      mock.db,
      {
        groupBy: { entityType: true },
        scope: "ALL",
      },
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      for (const entry of result.value.summary) {
        expect(entry).not.toHaveProperty("oldValue");
        expect(entry).not.toHaveProperty("newValue");
        expect(entry).not.toHaveProperty("fieldName");
      }
    }
  });
});
