import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { ActivityLoggingInterceptor } from "./activity-logging.interceptor";
import { ActivityService } from "../activity.service";
import { ExecutionContext, CallHandler } from "@nestjs/common";
import { of } from "rxjs";
import { ActivityAction, EntityType } from "@prisma/client";

describe("ActivityLoggingInterceptor", () => {
  let interceptor: ActivityLoggingInterceptor;
  let activityService: ActivityService;

  const mockActivityService = {
    logActivity: vi.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ActivityLoggingInterceptor,
        {
          provide: ActivityService,
          useValue: mockActivityService,
        },
      ],
    }).compile();

    interceptor = module.get<ActivityLoggingInterceptor>(ActivityLoggingInterceptor);
    activityService = module.get<ActivityService>(ActivityService);

    vi.clearAllMocks();
  });

  const createMockExecutionContext = (
    method: string,
    params: any = {},
    body: any = {},
    user: any = null,
    ip = "127.0.0.1",
    userAgent = "test-agent"
  ): ExecutionContext => {
    return {
      switchToHttp: () => ({
        getRequest: () => ({
          method,
          params,
          body,
          user,
          ip,
          headers: {
            "user-agent": userAgent,
          },
        }),
      }),
      getClass: () => ({ name: "TestController" }),
      getHandler: () => ({ name: "testMethod" }),
    } as any;
  };

  const createMockCallHandler = (result: any = {}): CallHandler => {
    return {
      handle: () => of(result),
    } as any;
  };

  describe("intercept", () => {
    it("should not log if user is not authenticated", async () => {
      const context = createMockExecutionContext("POST", {}, {}, null);
      const next = createMockCallHandler();

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          expect(mockActivityService.logActivity).not.toHaveBeenCalled();
          resolve();
        });
      });
    });

    it("should log POST request as CREATE action", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const body = {
        title: "New Task",
      };

      const result = {
        id: "task-123",
        workspaceId: "workspace-123",
        title: "New Task",
      };

      const context = createMockExecutionContext(
        "POST",
        {},
        body,
        user,
        "127.0.0.1",
        "Mozilla/5.0"
      );
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-123",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          expect(mockActivityService.logActivity).toHaveBeenCalledWith({
            workspaceId: "workspace-123",
            userId: "user-123",
            action: ActivityAction.CREATED,
            entityType: expect.any(String),
            entityId: "task-123",
            details: expect.objectContaining({
              method: "POST",
              controller: "TestController",
              handler: "testMethod",
            }),
            ipAddress: "127.0.0.1",
            userAgent: "Mozilla/5.0",
          });
          resolve();
        });
      });
    });

    it("should log PATCH request as UPDATE action", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const params = {
        id: "task-456",
      };

      const body = {
        status: "IN_PROGRESS",
      };

      const result = {
        id: "task-456",
        workspaceId: "workspace-123",
        status: "IN_PROGRESS",
      };

      const context = createMockExecutionContext("PATCH", params, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-124",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          expect(mockActivityService.logActivity).toHaveBeenCalledWith({
            workspaceId: "workspace-123",
            userId: "user-123",
            action: ActivityAction.UPDATED,
            entityType: expect.any(String),
            entityId: "task-456",
            details: expect.objectContaining({
              method: "PATCH",
              changes: body,
            }),
            ipAddress: "127.0.0.1",
            userAgent: "test-agent",
          });
          resolve();
        });
      });
    });

    it("should log PUT request as UPDATE action", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const params = {
        id: "event-789",
      };

      const body = {
        title: "Updated Event",
      };

      const result = {
        id: "event-789",
        workspaceId: "workspace-123",
        title: "Updated Event",
      };

      const context = createMockExecutionContext("PUT", params, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-125",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          expect(mockActivityService.logActivity).toHaveBeenCalledWith({
            workspaceId: "workspace-123",
            userId: "user-123",
            action: ActivityAction.UPDATED,
            entityType: expect.any(String),
            entityId: "event-789",
            details: expect.objectContaining({
              method: "PUT",
            }),
            ipAddress: "127.0.0.1",
            userAgent: "test-agent",
          });
          resolve();
        });
      });
    });

    it("should log DELETE request as DELETE action", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const params = {
        id: "project-999",
      };

      const result = {
        id: "project-999",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("DELETE", params, {}, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-126",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          expect(mockActivityService.logActivity).toHaveBeenCalledWith({
            workspaceId: "workspace-123",
            userId: "user-123",
            action: ActivityAction.DELETED,
            entityType: expect.any(String),
            entityId: "project-999",
            details: expect.objectContaining({
              method: "DELETE",
            }),
            ipAddress: "127.0.0.1",
            userAgent: "test-agent",
          });
          resolve();
        });
      });
    });

    it("should not log GET requests", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("GET", {}, {}, user);
      const next = createMockCallHandler({ data: [] });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          expect(mockActivityService.logActivity).not.toHaveBeenCalled();
          resolve();
        });
      });
    });

    it("should extract entity ID from result if not in params", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const body = {
        title: "New Task",
      };

      const result = {
        id: "task-new-123",
        workspaceId: "workspace-123",
        title: "New Task",
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-127",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          expect(mockActivityService.logActivity).toHaveBeenCalledWith(
            expect.objectContaining({
              entityId: "task-new-123",
            })
          );
          resolve();
        });
      });
    });

    it("should handle errors gracefully", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("POST", {}, {}, user);
      const next = createMockCallHandler({ id: "test-123" });

      mockActivityService.logActivity.mockRejectedValue(new Error("Logging failed"));

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          // Should not throw error, just log it
          resolve();
        });
      });
    });
  });

  describe("edge cases", () => {
    it("should handle POST request with no id field in response", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const body = {
        title: "New Task",
      };

      const result = {
        workspaceId: "workspace-123",
        title: "New Task",
        // No 'id' field in response
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-123",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          // Should not call logActivity when entityId is missing
          expect(mockActivityService.logActivity).not.toHaveBeenCalled();
          resolve();
        });
      });
    });

    it("should handle user object missing workspaceId", async () => {
      const user = {
        id: "user-123",
        // No workspaceId
      };

      const body = {
        title: "New Task",
      };

      const result = {
        id: "task-123",
        title: "New Task",
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-123",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          // workspaceId is now optional, so logActivity should be called without it
          expect(mockActivityService.logActivity).toHaveBeenCalled();
          const callArgs = mockActivityService.logActivity.mock.calls[0][0];
          expect(callArgs.userId).toBe("user-123");
          expect(callArgs.entityId).toBe("task-123");
          expect(callArgs.workspaceId).toBeUndefined();
          resolve();
        });
      });
    });

    it("should handle body missing workspaceId when user also missing workspaceId", async () => {
      const user = {
        id: "user-123",
        // No workspaceId
      };

      const body = {
        title: "New Task",
        // No workspaceId
      };

      const result = {
        id: "task-123",
        title: "New Task",
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-123",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          // workspaceId is now optional, so logActivity should be called without it
          expect(mockActivityService.logActivity).toHaveBeenCalled();
          const callArgs = mockActivityService.logActivity.mock.calls[0][0];
          expect(callArgs.userId).toBe("user-123");
          expect(callArgs.entityId).toBe("task-123");
          expect(callArgs.workspaceId).toBeUndefined();
          resolve();
        });
      });
    });

    it("should extract workspaceId from body when not in user object", async () => {
      const user = {
        id: "user-123",
        // No workspaceId
      };

      const body = {
        workspaceId: "workspace-from-body",
        title: "New Task",
      };

      const result = {
        id: "task-123",
        title: "New Task",
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-123",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          expect(mockActivityService.logActivity).toHaveBeenCalledWith(
            expect.objectContaining({
              workspaceId: "workspace-from-body",
            })
          );
          resolve();
        });
      });
    });

    it("should handle null result from handler", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("DELETE", { id: "task-123" }, {}, user);
      const next = createMockCallHandler(null);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-123",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          // Should still log activity with entityId from params
          expect(mockActivityService.logActivity).toHaveBeenCalledWith(
            expect.objectContaining({
              entityId: "task-123",
              workspaceId: "workspace-123",
            })
          );
          resolve();
        });
      });
    });

    it("should handle undefined result from handler", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("POST", {}, { title: "New Task" }, user);
      const next = createMockCallHandler(undefined);

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          // Should not log when entityId cannot be determined
          expect(mockActivityService.logActivity).not.toHaveBeenCalled();
          resolve();
        });
      });
    });

    it("should log warning when entityId is missing", async () => {
      const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const body = {
        title: "New Task",
      };

      const result = {
        workspaceId: "workspace-123",
        title: "New Task",
        // No 'id' field
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          resolve();
        });
      });

      consoleSpy.mockRestore();
    });

    it("should log warning when workspaceId is missing", async () => {
      const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

      const user = {
        id: "user-123",
        // No workspaceId
      };

      const body = {
        title: "New Task",
      };

      const result = {
        id: "task-123",
        title: "New Task",
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          resolve();
        });
      });

      consoleSpy.mockRestore();
    });

    it("should handle activity service throwing an error", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("POST", {}, {}, user);
      const next = createMockCallHandler({ id: "test-123" });

      const activityError = new Error("Activity logging failed");
      mockActivityService.logActivity.mockRejectedValue(activityError);

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          // Should not throw error, just log it
          resolve();
        });
      });
    });

    it("should handle OPTIONS requests", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("OPTIONS", {}, {}, user);
      const next = createMockCallHandler({});

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          // Should not log OPTIONS requests
          expect(mockActivityService.logActivity).not.toHaveBeenCalled();
          resolve();
        });
      });
    });

    it("should handle HEAD requests", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("HEAD", {}, {}, user);
      const next = createMockCallHandler({});

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          // Should not log HEAD requests
          expect(mockActivityService.logActivity).not.toHaveBeenCalled();
          resolve();
        });
      });
    });
  });

  describe("sensitive data sanitization", () => {
    it("should redact password field", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const body = {
        username: "testuser",
        password: "secret123",
        email: "test@example.com",
      };

      const result = {
        id: "user-456",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-123",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          const logCall = mockActivityService.logActivity.mock.calls[0][0];
          expect(logCall.details.data.password).toBe("[REDACTED]");
          expect(logCall.details.data.username).toBe("testuser");
          expect(logCall.details.data.email).toBe("test@example.com");
          resolve();
        });
      });
    });

    it("should redact token field", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const body = {
        title: "Integration",
        apiToken: "sk_test_1234567890",
      };

      const result = {
        id: "integration-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-124",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          const logCall = mockActivityService.logActivity.mock.calls[0][0];
          expect(logCall.details.data.apiToken).toBe("[REDACTED]");
          expect(logCall.details.data.title).toBe("Integration");
          resolve();
        });
      });
    });

    it("should redact sensitive fields in nested objects", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const body = {
        title: "Config",
        settings: {
          apiKey: "secret_key",
          public: "visible_data",
          auth: {
            token: "auth_token_123",
            refreshToken: "refresh_token_456",
          },
        },
      };

      const result = {
        id: "config-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-128",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          const logCall = mockActivityService.logActivity.mock.calls[0][0];
          expect(logCall.details.data.title).toBe("Config");
          expect(logCall.details.data.settings.apiKey).toBe("[REDACTED]");
          expect(logCall.details.data.settings.public).toBe("visible_data");
          expect(logCall.details.data.settings.auth.token).toBe("[REDACTED]");
          expect(logCall.details.data.settings.auth.refreshToken).toBe("[REDACTED]");
          resolve();
        });
      });
    });

    it("should not modify non-sensitive fields", async () => {
      const user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };

      const body = {
        title: "Safe Data",
        description: "This is public",
        count: 42,
        active: true,
      };

      const result = {
        id: "item-123",
        workspaceId: "workspace-123",
      };

      const context = createMockExecutionContext("POST", {}, body, user);
      const next = createMockCallHandler(result);

      mockActivityService.logActivity.mockResolvedValue({
        id: "activity-130",
      });

      await new Promise<void>((resolve) => {
        interceptor.intercept(context, next).subscribe(() => {
          const logCall = mockActivityService.logActivity.mock.calls[0][0];
          expect(logCall.details.data).toEqual(body);
          resolve();
        });
      });
    });
  });
});
