import { ConfigService } from "@nestjs/config";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { SecretScannerService } from "./secret-scanner.service";
import { SecretsDetectedError } from "./types";

describe("SecretScannerService", () => {
  let service: SecretScannerService;
  let mockConfigService: ConfigService;

  beforeEach(() => {
    // Reset all mocks
    vi.clearAllMocks();

    // Create mock config service
    mockConfigService = {
      get: vi.fn().mockReturnValue(undefined),
    } as unknown as ConfigService;

    // Create service with mock
    service = new SecretScannerService(mockConfigService);
  });

  it("should be defined", () => {
    expect(service).toBeDefined();
  });

  describe("scanContent", () => {
    describe("AWS Access Keys", () => {
      it("should detect real AWS access keys", () => {
        const content = 'const AWS_KEY = "AKIAREALKEY123456789";';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
        expect(result.count).toBe(1);
        expect(result.matches).toHaveLength(1);
        expect(result.matches[0].patternName).toBe("AWS Access Key");
        expect(result.matches[0].severity).toBe("critical");
      });

      it("should not detect fake AWS keys with wrong format", () => {
        const content = 'const FAKE_KEY = "AKIA1234";'; // Too short
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
        expect(result.count).toBe(0);
      });
    });

    describe("Claude API Keys", () => {
      it("should detect Claude API keys", () => {
        const content = 'CLAUDE_API_KEY="sk-ant-abc123def456ghi789jkl012mno345pqr678stu901vwx";';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
        expect(result.count).toBeGreaterThan(0);
        const claudeMatch = result.matches.find((m) => m.patternName.includes("Claude"));
        expect(claudeMatch).toBeDefined();
        expect(claudeMatch?.severity).toBe("critical");
      });

      it("should not detect placeholder Claude keys", () => {
        const content = 'CLAUDE_API_KEY="sk-ant-xxxx-your-key-here"';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
      });
    });

    describe("Generic API Keys", () => {
      it("should detect API keys with various formats", () => {
        const testCases = [
          'api_key = "abc123def456"',
          "apiKey: 'xyz789uvw123'",
          'API_KEY="prod123key456"',
        ];

        testCases.forEach((testCase) => {
          const result = service.scanContent(testCase);
          expect(result.hasSecrets).toBe(true);
        });
      });
    });

    describe("Passwords", () => {
      it("should detect password assignments", () => {
        const content = 'password = "mySecretPassword123"';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
        const passwordMatch = result.matches.find((m) =>
          m.patternName.toLowerCase().includes("password")
        );
        expect(passwordMatch).toBeDefined();
      });

      it("should not detect password placeholders", () => {
        const content = 'password = "your-password-here"';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
      });
    });

    describe("Private Keys", () => {
      it("should detect RSA private keys", () => {
        const content = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA1234567890abcdef
-----END RSA PRIVATE KEY-----`;
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
        const privateKeyMatch = result.matches.find((m) =>
          m.patternName.toLowerCase().includes("private key")
        );
        expect(privateKeyMatch).toBeDefined();
        expect(privateKeyMatch?.severity).toBe("critical");
      });

      it("should detect various private key types", () => {
        const keyTypes = [
          "RSA PRIVATE KEY",
          "PRIVATE KEY",
          "EC PRIVATE KEY",
          "OPENSSH PRIVATE KEY",
        ];

        keyTypes.forEach((keyType) => {
          const content = `-----BEGIN ${keyType}-----
MIIEpAIBAAKCAQEA1234567890abcdef
-----END ${keyType}-----`;
          const result = service.scanContent(content);
          expect(result.hasSecrets).toBe(true);
        });
      });
    });

    describe("JWT Tokens", () => {
      it("should detect JWT tokens", () => {
        const content =
          'token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
        const jwtMatch = result.matches.find((m) => m.patternName.toLowerCase().includes("jwt"));
        expect(jwtMatch).toBeDefined();
      });
    });

    describe("Bearer Tokens", () => {
      it("should detect Bearer tokens", () => {
        const content = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
        const bearerMatch = result.matches.find((m) =>
          m.patternName.toLowerCase().includes("bearer")
        );
        expect(bearerMatch).toBeDefined();
      });
    });

    describe("Multiple Secrets", () => {
      it("should detect multiple secrets in the same content", () => {
        const content = `
const config = {
  awsKey: "AKIAREALKEY123456789",
  apiKey: "abc123def456",
  password: "mySecret123"
};
        `;
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
        expect(result.count).toBeGreaterThanOrEqual(3);
      });
    });

    describe("Line and Column Tracking", () => {
      it("should track line numbers correctly", () => {
        const content = `line 1
line 2
const secret = "AKIAREALKEY123456789";
line 4`;
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
        expect(result.matches[0].line).toBe(3);
        expect(result.matches[0].column).toBeGreaterThan(0);
      });

      it("should provide context for matches", () => {
        const content = 'const key = "AKIAREALKEY123456789";';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
        expect(result.matches[0].context).toBeDefined();
      });
    });

    describe("Clean Content", () => {
      it("should return no secrets for clean content", () => {
        const content = `
const greeting = "Hello World";
const number = 42;
function add(a, b) { return a + b; }
        `;
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
        expect(result.count).toBe(0);
        expect(result.matches).toHaveLength(0);
      });

      it("should handle empty content", () => {
        const result = service.scanContent("");

        expect(result.hasSecrets).toBe(false);
        expect(result.count).toBe(0);
      });
    });

    describe("Whitelisting", () => {
      it("should not flag .env.example placeholder values", () => {
        const content = `
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
API_KEY=your-api-key-here
SECRET_KEY=xxxxxxxxxxxx
        `;
        const result = service.scanContent(content, ".env.example");

        expect(result.hasSecrets).toBe(false);
      });

      it("should flag real secrets even in .env files", () => {
        const content = 'API_KEY="AKIAIOSFODNN7REALKEY123"';
        const result = service.scanContent(content, ".env");

        expect(result.hasSecrets).toBe(true);
      });

      it("should whitelist placeholders in example files", () => {
        const content = 'API_KEY="xxxxxxxxxxxx"';
        const result = service.scanContent(content, "config.example.ts");

        expect(result.hasSecrets).toBe(false);
      });

      it("should whitelist obvious placeholders like xxxx", () => {
        const content = 'secret="xxxxxxxxxxxxxxxxxxxx"';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
      });

      it("should whitelist your-*-here patterns", () => {
        const content = 'secret="your-secret-here"';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
      });

      it("should whitelist AWS EXAMPLE keys (official AWS documentation)", () => {
        const content = 'const AWS_KEY = "AKIAIOSFODNN7EXAMPLE";';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
      });

      it("should whitelist AWS keys with TEST suffix", () => {
        const content = "AWS_ACCESS_KEY_ID=AKIATESTSECRET123456";
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
      });

      it("should whitelist AWS keys with SAMPLE suffix", () => {
        const content = 'key="AKIASAMPLEKEY1234567"';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
      });

      it("should whitelist AWS keys with DEMO suffix", () => {
        const content = 'const demo = "AKIADEMOKEY123456789";';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
      });

      it("should still detect real AWS keys without example markers", () => {
        const content = "AWS_ACCESS_KEY_ID=AKIAREALKEY123456789";
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
      });

      it("should whitelist test/demo/sample placeholder patterns", () => {
        const testCases = [
          'password="test-password-123"',
          'api_key="demo-api-key"',
          'secret="sample-secret-value"',
        ];

        testCases.forEach((testCase) => {
          const result = service.scanContent(testCase);
          expect(result.hasSecrets).toBe(false);
        });
      });

      it("should whitelist multiple xxxx patterns", () => {
        const content = 'token="xxxx-some-text-xxxx"';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(false);
      });

      it("should not whitelist real secrets just because they contain word test", () => {
        // "test" in the key name should not whitelist the actual secret value
        const content = 'test_password="MyRealPassword123"';
        const result = service.scanContent(content);

        expect(result.hasSecrets).toBe(true);
      });

      it("should handle case-insensitive EXAMPLE detection", () => {
        const testCases = [
          'key="AKIAexample12345678"',
          'key="AKIAEXAMPLE12345678"',
          'key="AKIAExample12345678"',
        ];

        testCases.forEach((testCase) => {
          const result = service.scanContent(testCase);
          expect(result.hasSecrets).toBe(false);
        });
      });

      it("should not flag placeholder secrets in example files even without obvious patterns", () => {
        const content = `
API_KEY=your-api-key-here
PASSWORD=change-me
SECRET=replace-me
        `;
        const result = service.scanContent(content, "config.example.yml");

        expect(result.hasSecrets).toBe(false);
      });
    });
  });

  describe("scanFile", () => {
    it("should scan a file and return results with secrets", async () => {
      // Create a temp file with secrets
      const fs = await import("fs/promises");
      const path = await import("path");
      const os = await import("os");

      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
      const testFile = path.join(tmpDir, "test.ts");

      await fs.writeFile(testFile, 'const key = "AKIAREALKEY123456789";\n');

      const result = await service.scanFile(testFile);

      expect(result.filePath).toBe(testFile);
      expect(result.hasSecrets).toBe(true);
      expect(result.count).toBeGreaterThan(0);

      // Cleanup
      await fs.unlink(testFile);
      await fs.rmdir(tmpDir);
    });

    it("should handle files without secrets", async () => {
      const fs = await import("fs/promises");
      const path = await import("path");
      const os = await import("os");

      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
      const testFile = path.join(tmpDir, "clean.ts");

      await fs.writeFile(testFile, 'const message = "Hello World";\n');

      const result = await service.scanFile(testFile);

      expect(result.filePath).toBe(testFile);
      expect(result.hasSecrets).toBe(false);
      expect(result.count).toBe(0);

      // Cleanup
      await fs.unlink(testFile);
      await fs.rmdir(tmpDir);
    });

    it("should return error state for non-existent files", async () => {
      const result = await service.scanFile("/non/existent/file.ts");

      expect(result.hasSecrets).toBe(false);
      expect(result.count).toBe(0);
      expect(result.scannedSuccessfully).toBe(false);
      expect(result.scanError).toBeDefined();
      expect(result.scanError).toContain("ENOENT");
    });

    it("should return scannedSuccessfully true for successful scans", async () => {
      const fs = await import("fs/promises");
      const path = await import("path");
      const os = await import("os");

      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
      const testFile = path.join(tmpDir, "clean.ts");

      await fs.writeFile(testFile, 'const message = "Hello World";\n');

      const result = await service.scanFile(testFile);

      expect(result.scannedSuccessfully).toBe(true);
      expect(result.scanError).toBeUndefined();

      // Cleanup
      await fs.unlink(testFile);
      await fs.rmdir(tmpDir);
    });

    it("should return error state for unreadable files", async () => {
      const fs = await import("fs/promises");
      const path = await import("path");
      const os = await import("os");

      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
      const testFile = path.join(tmpDir, "unreadable.ts");

      await fs.writeFile(testFile, 'const key = "AKIAREALKEY123456789";\n');
      // Remove read permissions
      await fs.chmod(testFile, 0o000);

      // Check if we can still read the file (e.g., running as root)
      let canReadAsRoot = false;
      try {
        await fs.readFile(testFile);
        canReadAsRoot = true;
      } catch {
        // Expected behavior for non-root users
      }

      const result = await service.scanFile(testFile);

      if (canReadAsRoot) {
        // Running as root - file is readable despite chmod 0o000
        // Scanner will successfully scan the file
        expect(result.scannedSuccessfully).toBe(true);
        expect(result.hasSecrets).toBe(true); // Contains AWS key
      } else {
        // Normal user - file is unreadable
        expect(result.scannedSuccessfully).toBe(false);
        expect(result.scanError).toBeDefined();
        expect(result.hasSecrets).toBe(false); // Not "clean", just unscanned
      }

      // Cleanup - restore permissions first
      await fs.chmod(testFile, 0o644);
      await fs.unlink(testFile);
      await fs.rmdir(tmpDir);
    });
  });

  describe("scanFiles", () => {
    it("should scan multiple files", async () => {
      const fs = await import("fs/promises");
      const path = await import("path");
      const os = await import("os");

      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
      const file1 = path.join(tmpDir, "file1.ts");
      const file2 = path.join(tmpDir, "file2.ts");

      await fs.writeFile(file1, 'const key = "AKIAREALKEY123456789";\n');
      await fs.writeFile(file2, 'const msg = "Hello";\n');

      const results = await service.scanFiles([file1, file2]);

      expect(results).toHaveLength(2);
      expect(results[0].hasSecrets).toBe(true);
      expect(results[1].hasSecrets).toBe(false);

      // Cleanup
      await fs.unlink(file1);
      await fs.unlink(file2);
      await fs.rmdir(tmpDir);
    });
  });

  describe("getScanSummary", () => {
    it("should provide summary of scan results", () => {
      const results = [
        {
          filePath: "file1.ts",
          hasSecrets: true,
          count: 2,
          scannedSuccessfully: true,
          matches: [
            {
              patternName: "AWS Access Key",
              match: "AKIA...",
              line: 1,
              column: 1,
              severity: "critical" as const,
            },
            {
              patternName: "API Key",
              match: "api_key",
              line: 2,
              column: 1,
              severity: "high" as const,
            },
          ],
        },
        {
          filePath: "file2.ts",
          hasSecrets: false,
          count: 0,
          scannedSuccessfully: true,
          matches: [],
        },
      ];

      const summary = service.getScanSummary(results);

      expect(summary.totalFiles).toBe(2);
      expect(summary.filesWithSecrets).toBe(1);
      expect(summary.totalSecrets).toBe(2);
      expect(summary.filesWithErrors).toBe(0);
      expect(summary.bySeverity.critical).toBe(1);
      expect(summary.bySeverity.high).toBe(1);
      expect(summary.bySeverity.medium).toBe(0);
    });

    it("should count files with scan errors", () => {
      const results = [
        {
          filePath: "file1.ts",
          hasSecrets: true,
          count: 1,
          scannedSuccessfully: true,
          matches: [
            {
              patternName: "AWS Access Key",
              match: "AKIA...",
              line: 1,
              column: 1,
              severity: "critical" as const,
            },
          ],
        },
        {
          filePath: "file2.ts",
          hasSecrets: false,
          count: 0,
          scannedSuccessfully: false,
          scanError: "ENOENT: no such file or directory",
          matches: [],
        },
        {
          filePath: "file3.ts",
          hasSecrets: false,
          count: 0,
          scannedSuccessfully: false,
          scanError: "EACCES: permission denied",
          matches: [],
        },
      ];

      const summary = service.getScanSummary(results);

      expect(summary.totalFiles).toBe(3);
      expect(summary.filesWithSecrets).toBe(1);
      expect(summary.filesWithErrors).toBe(2);
      expect(summary.totalSecrets).toBe(1);
    });
  });

  describe("SecretsDetectedError", () => {
    it("should create error with results", () => {
      const results = [
        {
          filePath: "test.ts",
          hasSecrets: true,
          count: 1,
          scannedSuccessfully: true,
          matches: [
            {
              patternName: "AWS Access Key",
              match: "AKIAREALKEY123456789",
              line: 1,
              column: 10,
              severity: "critical" as const,
            },
          ],
        },
      ];

      const error = new SecretsDetectedError(results);

      expect(error.results).toBe(results);
      expect(error.message).toContain("Secrets detected");
    });

    it("should provide detailed error message", () => {
      const results = [
        {
          filePath: "config.ts",
          hasSecrets: true,
          count: 1,
          scannedSuccessfully: true,
          matches: [
            {
              patternName: "API Key",
              match: "abc123",
              line: 5,
              column: 15,
              severity: "high" as const,
              context: 'const apiKey = "abc123"',
            },
          ],
        },
      ];

      const error = new SecretsDetectedError(results);
      const detailed = error.getDetailedMessage();

      expect(detailed).toContain("SECRETS DETECTED");
      expect(detailed).toContain("config.ts");
      expect(detailed).toContain("Line 5:15");
      expect(detailed).toContain("API Key");
    });

    it("should include scan errors in detailed message", () => {
      const results = [
        {
          filePath: "config.ts",
          hasSecrets: true,
          count: 1,
          scannedSuccessfully: true,
          matches: [
            {
              patternName: "API Key",
              match: "abc123",
              line: 5,
              column: 15,
              severity: "high" as const,
              context: 'const apiKey = "abc123"',
            },
          ],
        },
        {
          filePath: "unreadable.ts",
          hasSecrets: false,
          count: 0,
          scannedSuccessfully: false,
          scanError: "EACCES: permission denied",
          matches: [],
        },
      ];

      const error = new SecretsDetectedError(results);
      const detailed = error.getDetailedMessage();

      expect(detailed).toContain("SECRETS DETECTED");
      expect(detailed).toContain("config.ts");
      expect(detailed).toContain("could not be scanned");
      expect(detailed).toContain("unreadable.ts");
      expect(detailed).toContain("EACCES: permission denied");
    });
  });

  describe("Custom Patterns", () => {
    it("should support adding custom patterns via config", () => {
      // Create service with custom patterns
      const customMockConfig = {
        get: vi.fn((key: string) => {
          if (key === "orchestrator.secretScanner.customPatterns") {
            return [
              {
                name: "Custom Token",
                pattern: /CUSTOM-[A-Z0-9]{10}/g,
                description: "Custom token pattern",
                severity: "high",
              },
            ];
          }
          return undefined;
        }),
      } as unknown as ConfigService;

      const customService = new SecretScannerService(customMockConfig);
      const result = customService.scanContent("token = CUSTOM-ABCD123456");

      expect(result.hasSecrets).toBe(true);
      expect(result.matches.some((m) => m.patternName === "Custom Token")).toBe(true);
    });

    it("should respect exclude patterns from config", async () => {
      const fs = await import("fs/promises");
      const path = await import("path");
      const os = await import("os");

      const excludeMockConfig = {
        get: vi.fn((key: string) => {
          if (key === "orchestrator.secretScanner.excludePatterns") {
            return ["*.test.ts"];
          }
          return undefined;
        }),
      } as unknown as ConfigService;

      const excludeService = new SecretScannerService(excludeMockConfig);

      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
      const testFile = path.join(tmpDir, "file.test.ts");

      await fs.writeFile(testFile, 'const key = "AKIAREALKEY123456789";\n');

      const result = await excludeService.scanFile(testFile);

      expect(result.hasSecrets).toBe(false); // Excluded files return no secrets

      // Cleanup
      await fs.unlink(testFile);
      await fs.rmdir(tmpDir);
    });

    it("should respect max file size limit", async () => {
      const fs = await import("fs/promises");
      const path = await import("path");
      const os = await import("os");

      const sizeMockConfig = {
        get: vi.fn((key: string) => {
          if (key === "orchestrator.secretScanner.maxFileSize") {
            return 10; // 10 bytes max
          }
          return undefined;
        }),
      } as unknown as ConfigService;

      const sizeService = new SecretScannerService(sizeMockConfig);

      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
      const testFile = path.join(tmpDir, "large.ts");

      // Create a file larger than 10 bytes
      await fs.writeFile(testFile, 'const key = "AKIAREALKEY123456789";\n');

      const result = await sizeService.scanFile(testFile);

      expect(result.hasSecrets).toBe(false); // Large files are skipped

      // Cleanup
      await fs.unlink(testFile);
      await fs.rmdir(tmpDir);
    });
  });

  describe("Edge Cases", () => {
    it("should handle very long lines", () => {
      const longLine = "a".repeat(10000) + 'key="AKIAREALKEY123456789"';
      const result = service.scanContent(longLine);

      expect(result.hasSecrets).toBe(true);
    });

    it("should handle multiline private keys correctly", () => {
      const content = `
Some text before
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA1234567890abcdef
ghijklmnopqrstuvwxyz123456789012
-----END RSA PRIVATE KEY-----
Some text after
      `;
      const result = service.scanContent(content);

      expect(result.hasSecrets).toBe(true);
      expect(result.count).toBeGreaterThan(0);
    });

    it("should handle content with special characters", () => {
      const content = 'key="AKIAREALKEY123456789" # Comment with émojis 🔑';
      const result = service.scanContent(content);

      expect(result.hasSecrets).toBe(true);
    });
  });
});
