import { Alepha } from "alepha";
import { DateTimeProvider } from "alepha/datetime";
import { $repository } from "alepha/orm";
import { AlephaOrmPostgres } from "alepha/orm/postgres";
import { describe, it } from "vitest";
import { AlephaApiAudits, AuditService, audits } from "../index.ts";

/**
 * Direct repository access so retention tests can backdate `createdAt` (which
 * the public `create` API does not expose).
 */
class Db {
  audits = $repository(audits);
}

const setup = async () => {
  const alepha = Alepha.create({
    env: { LOG_LEVEL: "error" },
  });

  alepha.with(AlephaOrmPostgres);
  alepha.with(AlephaApiAudits);

  // Inject before start: the container locks once started, so test-only
  // services (Db) must be registered first.
  const db = alepha.inject(Db);
  const time = alepha.inject(DateTimeProvider);

  await alepha.start();

  const auditService = alepha.inject(AuditService);

  return {
    alepha,
    auditService,
    db,
    time,
  };
};

describe("alepha/api/audits - AuditService", () => {
  describe("create", () => {
    it("should create a basic audit entry", async ({ expect }) => {
      const { auditService } = await setup();

      const entry = await auditService.create({
        type: "test",
        action: "create",
        description: "Test audit entry",
      });

      expect(entry.id).toBeDefined();
      expect(entry.type).toBe("test");
      expect(entry.action).toBe("create");
      expect(entry.description).toBe("Test audit entry");
      expect(entry.severity).toBe("info");
      expect(entry.success).toBe(true);
      expect(entry.createdAt).toBeDefined();
    });

    it("should create audit entry with all fields", async ({ expect }) => {
      const { auditService } = await setup();

      const entry = await auditService.create({
        type: "user",
        action: "update",
        severity: "warning",
        userId: "550e8400-e29b-41d4-a716-446655440000",
        userRealm: "admin",
        userEmail: "admin@example.com",
        resourceType: "user",
        resourceId: "660e8400-e29b-41d4-a716-446655440001",
        description: "Updated user profile",
        metadata: { field: "email", oldValue: "old@test.com" },
        ipAddress: "192.168.1.1",
        userAgent: "Mozilla/5.0",
        sessionId: "770e8400-e29b-41d4-a716-446655440002",
        requestId: "req-123",
        success: true,
      });

      expect(entry.type).toBe("user");
      expect(entry.action).toBe("update");
      expect(entry.severity).toBe("warning");
      expect(entry.userId).toBe("550e8400-e29b-41d4-a716-446655440000");
      expect(entry.userRealm).toBe("admin");
      expect(entry.userEmail).toBe("admin@example.com");
      expect(entry.resourceType).toBe("user");
      expect(entry.resourceId).toBe("660e8400-e29b-41d4-a716-446655440001");
      expect(entry.metadata).toEqual({
        field: "email",
        oldValue: "old@test.com",
      });
      expect(entry.ipAddress).toBe("192.168.1.1");
      expect(entry.userAgent).toBe("Mozilla/5.0");
      expect(entry.sessionId).toBe("770e8400-e29b-41d4-a716-446655440002");
      expect(entry.requestId).toBe("req-123");
    });

    it("should create audit entry with failure status", async ({ expect }) => {
      const { auditService } = await setup();

      const entry = await auditService.create({
        type: "auth",
        action: "login",
        success: false,
        errorMessage: "Invalid credentials",
      });

      expect(entry.success).toBe(false);
      expect(entry.errorMessage).toBe("Invalid credentials");
    });
  });

  describe("record", () => {
    it("should record audit event using convenience method", async ({
      expect,
    }) => {
      const { auditService } = await setup();

      const entry = await auditService.record("payment", "create", {
        userId: "550e8400-e29b-41d4-a716-446655440000",
        resourceType: "payment",
        resourceId: "pay-123",
        metadata: { amount: 99.99 },
      });

      expect(entry.type).toBe("payment");
      expect(entry.action).toBe("create");
      expect(entry.resourceType).toBe("payment");
      expect(entry.resourceId).toBe("pay-123");
    });
  });

  describe("severity from success", () => {
    it("defaults a failed audit to warning severity", async ({ expect }) => {
      const { auditService } = await setup();

      const entry = await auditService.create({
        type: "auth",
        action: "login",
        success: false,
        errorMessage: "Invalid password",
      });

      expect(entry.success).toBe(false);
      expect(entry.severity).toBe("warning");
    });

    it("defaults a successful audit to info severity", async ({ expect }) => {
      const { auditService } = await setup();

      const entry = await auditService.create({
        type: "auth",
        action: "login",
      });

      expect(entry.severity).toBe("info");
    });

    it("respects an explicit severity over the success default", async ({
      expect,
    }) => {
      const { auditService } = await setup();

      const entry = await auditService.create({
        type: "security",
        action: "sessions_invalidated",
        severity: "warning",
      });

      expect(entry.severity).toBe("warning");
    });
  });

  describe("find", () => {
    it("should find audit entries with pagination", async ({ expect }) => {
      const { auditService } = await setup();

      // Create multiple entries
      await auditService.create({ type: "test", action: "action1" });
      await auditService.create({ type: "test", action: "action2" });
      await auditService.create({ type: "test", action: "action3" });

      const result = await auditService.find({ size: 2 });

      expect(result.content.length).toBeLessThanOrEqual(2);
      expect(result.page).toBeDefined();
    });

    it("should filter by type", async ({ expect }) => {
      const { auditService } = await setup();

      await auditService.create({ type: "auth", action: "login" });
      await auditService.create({ type: "user", action: "create" });

      const result = await auditService.find({ type: "auth" });

      expect(result.content.every((e) => e.type === "auth")).toBe(true);
    });

    it("should filter by severity", async ({ expect }) => {
      const { auditService } = await setup();

      await auditService.create({
        type: "test",
        action: "info",
        severity: "info",
      });
      await auditService.create({
        type: "test",
        action: "warning",
        severity: "warning",
      });

      const result = await auditService.find({ severity: "warning" });

      expect(result.content.every((e) => e.severity === "warning")).toBe(true);
    });

    it("should filter by userId", async ({ expect }) => {
      const { auditService } = await setup();
      const userId = "550e8400-e29b-41d4-a716-446655440000";

      await auditService.create({ type: "test", action: "a", userId });
      await auditService.create({ type: "test", action: "b" });

      const result = await auditService.find({ userId });

      expect(result.content.every((e) => e.userId === userId)).toBe(true);
    });

    it("should filter by success status", async ({ expect }) => {
      const { auditService } = await setup();

      await auditService.create({
        type: "test",
        action: "success",
        success: true,
      });
      await auditService.create({
        type: "test",
        action: "failure",
        success: false,
      });

      const failures = await auditService.find({ success: false });

      expect(failures.content.every((e) => e.success === false)).toBe(true);
    });

    it("should sort by createdAt descending by default", async ({ expect }) => {
      const { auditService } = await setup();

      await auditService.create({ type: "test", action: "first" });
      await new Promise((r) => setTimeout(r, 10));
      await auditService.create({ type: "test", action: "second" });

      const result = await auditService.find({ type: "test" });

      if (result.content.length >= 2) {
        const first = new Date(result.content[0].createdAt).getTime();
        const second = new Date(result.content[1].createdAt).getTime();
        expect(first).toBeGreaterThanOrEqual(second);
      }
    });
  });

  describe("findByUser", () => {
    it("should find audit entries for a specific user", async ({ expect }) => {
      const { auditService } = await setup();
      const userId = "550e8400-e29b-41d4-a716-446655440000";

      await auditService.create({ type: "auth", action: "login", userId });
      await auditService.create({ type: "user", action: "update", userId });

      const result = await auditService.findByUser(userId);

      expect(result.content.every((e) => e.userId === userId)).toBe(true);
    });
  });

  describe("findByResource", () => {
    it("should find audit entries for a specific resource", async ({
      expect,
    }) => {
      const { auditService } = await setup();

      await auditService.create({
        type: "user",
        action: "create",
        resourceType: "user",
        resourceId: "user-123",
      });
      await auditService.create({
        type: "user",
        action: "update",
        resourceType: "user",
        resourceId: "user-123",
      });

      const result = await auditService.findByResource("user", "user-123");

      expect(
        result.content.every(
          (e) => e.resourceType === "user" && e.resourceId === "user-123",
        ),
      ).toBe(true);
    });
  });

  describe("getStats", () => {
    it("should return audit statistics", async ({ expect }) => {
      const { auditService } = await setup();

      // Create diverse entries
      await auditService.create({
        type: "auth",
        action: "login",
        success: true,
      });
      await auditService.create({
        type: "auth",
        action: "login",
        severity: "warning",
        success: false,
      });
      await auditService.create({
        type: "user",
        action: "create",
        success: true,
      });
      await auditService.create({
        type: "system",
        action: "error",
        severity: "critical",
        success: false,
      });

      const stats = await auditService.getStats();

      expect(stats.total).toBeGreaterThanOrEqual(4);
      expect(stats.byType).toBeDefined();
      expect(stats.bySeverity).toBeDefined();
      expect(stats.bySeverity.info).toBeGreaterThanOrEqual(0);
      expect(stats.bySeverity.warning).toBeGreaterThanOrEqual(0);
      expect(stats.bySeverity.critical).toBeGreaterThanOrEqual(0);
      expect(stats.successRate).toBeDefined();
      expect(stats.recentFailures).toBeDefined();
    });

    it("should calculate correct success rate", async ({ expect }) => {
      const { auditService } = await setup();

      // Create 3 successful and 1 failed
      await auditService.create({
        type: "stats",
        action: "test1",
        success: true,
      });
      await auditService.create({
        type: "stats",
        action: "test2",
        success: true,
      });
      await auditService.create({
        type: "stats",
        action: "test3",
        success: true,
      });
      await auditService.create({
        type: "stats",
        action: "test4",
        success: false,
      });

      const stats = await auditService.getStats();

      // Success rate should be around 75% for the entries we created
      // (but there might be other entries from other tests)
      expect(stats.successRate).toBeGreaterThan(0);
      expect(stats.successRate).toBeLessThanOrEqual(1);
    });
  });

  describe("registerType", () => {
    it("should register audit types", async ({ expect }) => {
      const { auditService } = await setup();

      auditService.registerType({
        type: "custom",
        description: "Custom audit type",
        actions: ["create", "update", "delete"],
      });

      const types = auditService.getRegisteredTypes();
      const customType = types.find((t) => t.type === "custom");

      expect(customType).toBeDefined();
      expect(customType?.description).toBe("Custom audit type");
      expect(customType?.actions).toEqual(["create", "update", "delete"]);
    });
  });

  describe("getById", () => {
    it("should retrieve audit entry by ID", async ({ expect }) => {
      const { auditService } = await setup();

      const created = await auditService.create({
        type: "test",
        action: "getById",
        description: "Test getById",
      });

      const retrieved = await auditService.getById(String(created.id));

      expect(retrieved.id).toBe(created.id);
      expect(retrieved.type).toBe("test");
      expect(retrieved.action).toBe("getById");
    });
  });

  describe("deleteExpired", () => {
    // Cutoffs are always computed from `now`, so backdating entries keeps the
    // delete window in the past — concurrent test files' recent rows are safe.
    const countByType = async (db: Db, type: string): Promise<number> =>
      (await db.audits.findMany({ where: { type: { eq: type } } })).length;

    it("deletes entries older than the default retention", async ({
      expect,
    }) => {
      const { auditService, db, time } = await setup();
      const type = "av-default";

      await db.audits.deleteMany({ type: { eq: type } });
      await db.audits.create({
        type,
        action: "old",
        createdAt: time.now().subtract(200, "day").toISOString(),
      });
      await db.audits.create({
        type,
        action: "recent",
        createdAt: time.nowISOString(),
      });

      const deleted = await auditService.deleteExpired(time.now().toDate(), 90);

      expect(deleted).toBeGreaterThanOrEqual(1);
      expect(await countByType(db, type)).toBe(1);
    });

    it("keeps entries within the default retention window", async ({
      expect,
    }) => {
      const { auditService, db, time } = await setup();
      const type = "av-within";

      await db.audits.deleteMany({ type: { eq: type } });
      await db.audits.create({
        type,
        action: "x",
        createdAt: time.now().subtract(10, "day").toISOString(),
      });

      await auditService.deleteExpired(time.now().toDate(), 90);

      expect(await countByType(db, type)).toBe(1);
    });

    it("applies a type's dedicated retention over the default", async ({
      expect,
    }) => {
      const { auditService, db, time } = await setup();

      auditService.registerType({
        type: "av-short",
        actions: ["x"],
        retentionDays: 7,
      });

      await db.audits.deleteMany({ type: { eq: "av-short" } });
      await db.audits.deleteMany({ type: { eq: "av-long" } });

      // 30 days old: beyond the 7-day override, within the 90-day default.
      await db.audits.create({
        type: "av-short",
        action: "x",
        createdAt: time.now().subtract(30, "day").toISOString(),
      });
      await db.audits.create({
        type: "av-long",
        action: "x",
        createdAt: time.now().subtract(30, "day").toISOString(),
      });

      await auditService.deleteExpired(time.now().toDate(), 90);

      expect(await countByType(db, "av-short")).toBe(0);
      expect(await countByType(db, "av-long")).toBe(1);
    });

    it("keeps a type forever when its retention is 0", async ({ expect }) => {
      const { auditService, db, time } = await setup();

      auditService.registerType({
        type: "av-permanent",
        actions: ["x"],
        retentionDays: 0,
      });

      await db.audits.deleteMany({ type: { eq: "av-permanent" } });
      await db.audits.create({
        type: "av-permanent",
        action: "x",
        createdAt: time.now().subtract(5000, "day").toISOString(),
      });

      await auditService.deleteExpired(time.now().toDate(), 90);

      expect(await countByType(db, "av-permanent")).toBe(1);
    });

    it("disables cleanup when the default retention is 0", async ({
      expect,
    }) => {
      const { auditService, db, time } = await setup();
      const type = "av-disabled";

      await db.audits.deleteMany({ type: { eq: type } });
      await db.audits.create({
        type,
        action: "x",
        createdAt: time.now().subtract(5000, "day").toISOString(),
      });

      const deleted = await auditService.deleteExpired(time.now().toDate(), 0);

      expect(deleted).toBe(0);
      expect(await countByType(db, type)).toBe(1);
    });
  });
});
