import { describe, expect, it } from "vitest";

import { compileLighthouseContext } from "./compiler";
import {
  buildComplianceContext,
  buildFilteredProviderContext,
  buildFindingSeveritySummaryContext,
  buildFindingStatusSummaryContext,
  buildProviderGroupContext,
  buildServiceSummaryContext,
} from "./contributions";
import { buildLighthousePageContext } from "./pages";
import { lighthouseContextEnvelopeSchema } from "./schema";
import { getApiLighthouseContextByteLength } from "./transport";

describe("lighthouseContextEnvelopeSchema", () => {
  describe("when validating an inline page context", () => {
    it("should accept a valid version 1 envelope", () => {
      // Given
      const envelope = {
        schemaVersion: 1,
        transport: "inline",
        items: [
          {
            kind: "page",
            id: "findings",
            source: "automatic",
            scopeKey: "findings:/findings",
            label: "Findings",
            path: "/findings",
            filters: { severity: ["critical"] },
          },
        ],
      };

      // When
      const result = lighthouseContextEnvelopeSchema.safeParse(envelope);

      // Then
      expect(result.success).toBe(true);
    });

    it("should accept every supported contextual item kind", () => {
      // Given
      const envelope = {
        schemaVersion: 1,
        transport: "inline",
        items: [
          {
            kind: "finding",
            id: "finding-1",
            source: "selection",
            scopeKey: "findings:/findings",
            label: "Selected finding",
            findingId: "finding-1",
            checkId: "check-1",
            severity: "critical",
          },
          {
            kind: "resource",
            id: "resource-1",
            source: "selection",
            scopeKey: "resources:/resources",
            label: "Selected resource",
            resourceId: "resource-1",
            service: "s3",
            failedFindingsCount: 4,
          },
          {
            kind: "compliance",
            id: "cis-1.5",
            source: "automatic",
            scopeKey: "compliance:/compliance",
            label: "CIS 1.5",
            framework: "cis_1.5_aws",
            score: 82,
            totals: { passed: 82, failed: 18, total: 100 },
          },
          {
            kind: "attack_path",
            id: "query-1",
            source: "automatic",
            scopeKey: "attack-paths:/attack-paths/query-builder",
            label: "Attack path query",
            scanId: "scan-1",
            queryId: "query-1",
            parameters: { region: "eu-west-1", limit: 10 },
            nodeCount: 12,
            edgeCount: 11,
          },
          {
            kind: "scan",
            id: "scans-summary",
            source: "automatic",
            scopeKey: "scans:/scans",
            label: "Visible scans",
            total: 25,
          },
          {
            kind: "provider",
            id: "providers-summary",
            source: "automatic",
            scopeKey: "providers:/providers",
            label: "Visible providers",
            total: 7,
          },
        ],
      };

      // When
      const result = lighthouseContextEnvelopeSchema.safeParse(envelope);

      // Then
      expect(result.success).toBe(true);
    });
  });

  describe("when context exceeds transport limits", () => {
    it("should reject filters containing more than 20 values", () => {
      // Given
      const envelope = {
        schemaVersion: 1,
        transport: "inline",
        items: [
          {
            kind: "page",
            id: "findings",
            source: "automatic",
            scopeKey: "findings:/findings",
            label: "Findings",
            path: "/findings",
            filters: {
              severity: Array.from({ length: 21 }, (_, index) => `${index}`),
            },
          },
        ],
      };

      // When
      const result = lighthouseContextEnvelopeSchema.safeParse(envelope);

      // Then
      expect(result.success).toBe(false);
    });

    it("should reject more than twelve context items", () => {
      // Given
      const item = {
        kind: "page",
        id: "findings",
        source: "automatic",
        scopeKey: "findings:/findings",
        label: "Findings",
        path: "/findings",
      };

      // When
      const result = lighthouseContextEnvelopeSchema.safeParse({
        schemaVersion: 1,
        transport: "inline",
        items: Array.from({ length: 13 }, (_, index) => ({
          ...item,
          id: `page-${index}`,
        })),
      });

      // Then
      expect(result.success).toBe(false);
    });

    it("should reject strings longer than 256 characters", () => {
      // Given / When
      const result = lighthouseContextEnvelopeSchema.safeParse({
        schemaVersion: 1,
        transport: "inline",
        items: [
          {
            kind: "page",
            id: "findings",
            source: "automatic",
            scopeKey: "findings:/findings",
            label: "x".repeat(257),
            path: "/findings",
          },
        ],
      });

      // Then
      expect(result.success).toBe(false);
    });

    it("should reject unknown item kinds", () => {
      // Given / When
      const result = lighthouseContextEnvelopeSchema.safeParse({
        schemaVersion: 1,
        transport: "inline",
        items: [
          {
            kind: "secret",
            id: "credentials",
            source: "automatic",
            scopeKey: "findings:/findings",
            label: "Credentials",
          },
        ],
      });

      // Then
      expect(result.success).toBe(false);
    });
  });
});

describe("compileLighthouseContext", () => {
  describe("when multiple contributors describe the same entity", () => {
    it("should deduplicate items by kind and id", () => {
      // Given
      const scopeKey = "findings:/findings";
      const items = [
        {
          kind: "page",
          id: "findings",
          source: "automatic",
          scopeKey,
          label: "Findings",
          path: "/findings",
        },
        {
          kind: "finding",
          id: "finding-1",
          source: "selection",
          scopeKey,
          label: "Selected finding",
          findingId: "finding-1",
          severity: "critical",
        },
        {
          kind: "finding",
          id: "finding-1",
          source: "selection",
          scopeKey,
          label: "Duplicate finding",
          findingId: "finding-1",
          severity: "critical",
        },
      ];

      // When
      const context = compileLighthouseContext(items, scopeKey);

      // Then
      expect(context?.items.map((item) => `${item.kind}:${item.id}`)).toEqual([
        "page:findings",
        "finding:finding-1",
      ]);
    });

    it("should retain the highest-priority duplicate", () => {
      // Given
      const scopeKey = "findings:/findings";
      const items = [
        {
          kind: "page",
          id: "findings",
          source: "automatic",
          scopeKey,
          label: "Findings",
          path: "/findings",
        },
        {
          kind: "finding",
          id: "finding-1",
          source: "selection",
          scopeKey,
          label: "Selected finding",
          findingId: "finding-1",
        },
        {
          kind: "finding",
          id: "finding-1",
          source: "focused",
          scopeKey,
          label: "Focused finding",
          findingId: "finding-1",
        },
      ];

      // When
      const context = compileLighthouseContext(items, scopeKey);

      // Then
      expect(context?.items[1]).toMatchObject({
        id: "finding-1",
        source: "focused",
        label: "Focused finding",
      });
    });
  });

  describe("when contributors arrive in render order", () => {
    it("should order page, focused, selection, and summary items deterministically", () => {
      // Given
      const scopeKey = "findings:/findings";
      const items = [
        {
          kind: "finding",
          id: "findings-summary",
          source: "automatic",
          scopeKey,
          label: "Visible findings",
          findingId: "summary",
          total: 42,
        },
        {
          kind: "finding",
          id: "finding-1",
          source: "selection",
          scopeKey,
          label: "Selected finding",
          findingId: "finding-1",
        },
        {
          kind: "finding",
          id: "finding-focused",
          source: "focused",
          scopeKey,
          label: "Focused finding",
          findingId: "finding-focused",
        },
        {
          kind: "page",
          id: "findings",
          source: "automatic",
          scopeKey,
          label: "Findings",
          path: "/findings",
        },
      ];

      // When
      const context = compileLighthouseContext(items, scopeKey);

      // Then
      expect(context?.items.map((item) => item.id)).toEqual([
        "findings",
        "finding-focused",
        "finding-1",
        "findings-summary",
      ]);
    });
  });

  describe("when serialized context exceeds the byte limit", () => {
    it("should drop lowest-priority items until the context fits", () => {
      // Given
      const scopeKey = "findings:/findings";
      const page = {
        kind: "page",
        id: "findings",
        source: "automatic",
        scopeKey,
        label: "Findings",
        path: "/findings",
      };
      const selection = {
        kind: "finding",
        id: "finding-1",
        source: "selection",
        scopeKey,
        label: "Selected finding",
        findingId: "finding-1",
      };
      const summaries = Array.from({ length: 6 }, (_, index) => ({
        kind: "finding",
        id: `summary-${index}`,
        source: "automatic",
        scopeKey,
        label: `Summary ${index} ${"x".repeat(240)}`,
        findingId: `summary-${index}`,
        checkId: `check-${index}-${"y".repeat(240)}`,
        providerUid: `provider-${index}-${"z".repeat(237)}`,
        total: index,
      }));

      // When
      const context = compileLighthouseContext(
        [page, selection, ...summaries],
        scopeKey,
      );

      // Then
      expect(context?.items.map((item) => item.id)).toEqual([
        "findings",
        "finding-1",
        "summary-0",
        "summary-1",
        "summary-2",
        "summary-3",
      ]);
    });

    it("should drop oversized selections while keeping the page", () => {
      // Given
      const scopeKey = "findings:/findings";
      const page = {
        kind: "page",
        id: "findings",
        source: "automatic",
        scopeKey,
        label: "Findings",
        path: "/findings",
      };
      const selections = Array.from({ length: 2 }, (_, index) => ({
        kind: "finding",
        id: `finding-${index}`,
        source: "selection",
        scopeKey,
        label: "x".repeat(256),
        findingId: "y".repeat(256),
        checkId: "z".repeat(256),
        severity: "s".repeat(256),
        status: "t".repeat(256),
        providerUid: "p".repeat(256),
        resourceUid: "r".repeat(256),
        region: "g".repeat(256),
      }));

      // When
      const context = compileLighthouseContext([...selections, page], scopeKey);

      // Then
      expect(context?.items.map((item) => item.id)).toEqual([
        "findings",
        "finding-0",
      ]);
    });
  });

  describe("when context exceeds the item limit", () => {
    it("should progressively drop only the lowest-priority items", () => {
      // Given
      const scopeKey = "findings:/findings";
      const page = {
        kind: "page",
        id: "findings",
        source: "automatic",
        scopeKey,
        label: "Findings",
        path: "/findings",
      };
      const focused = {
        kind: "finding",
        id: "focused",
        source: "focused",
        scopeKey,
        label: "Focused finding",
        findingId: "focused",
      };
      const selections = Array.from({ length: 2 }, (_, index) => ({
        kind: "finding",
        id: `selection-${index}`,
        source: "selection",
        scopeKey,
        label: `Selected finding ${index}`,
        findingId: `selection-${index}`,
      }));
      const summaries = Array.from({ length: 12 }, (_, index) => ({
        kind: "finding",
        id: `summary-${index}`,
        source: "automatic",
        scopeKey,
        label: `Summary ${index}`,
        findingId: `summary-${index}`,
        total: index,
      }));

      // When
      const context = compileLighthouseContext(
        [page, ...summaries, ...selections, focused],
        scopeKey,
      );

      // Then
      expect(context?.items.map((item) => item.id)).toEqual([
        "findings",
        "focused",
        "selection-0",
        "selection-1",
        "summary-0",
        "summary-1",
        "summary-2",
        "summary-3",
        "summary-4",
        "summary-5",
        "summary-6",
        "summary-7",
      ]);
    });
  });

  describe("when the Overview publishes every contributor", () => {
    it("should fit a fully populated Overview within transport limits", () => {
      // Given every real Overview contributor plus the page item
      const candidates = [
        buildLighthousePageContext(
          "/",
          new URLSearchParams(
            "filter[provider_id__in]=b81165a0-4f28-4b5c-9a41-1e2d3c4b5a69&filter[provider_type__in]=aws",
          ),
        ),
        buildComplianceContext({
          pathname: "/",
          id: "prowler-threat-score",
          framework: "Prowler ThreatScore",
          score: 62.4,
          scoreDelta: -3.21,
          criticalRequirementsCount: 5,
          worstSection: "1.2 Attack Surface",
          worstSectionScore: 38.6,
          passed: 120,
          failed: 40,
          total: 160,
        }),
        buildFindingStatusSummaryContext({
          pathname: "/",
          passed: 320,
          failed: 80,
          newPassed: 12,
          newFailed: 7,
        }),
        buildFindingSeveritySummaryContext({
          pathname: "/",
          severityCounts: {
            critical: 4,
            high: 18,
            medium: 40,
            low: 15,
            informational: 3,
          },
        }),
        buildComplianceContext({
          pathname: "/",
          id: "watchlist-ens_rd2022_aws",
          framework: "ENS RD2022",
          score: 30,
        }),
        buildComplianceContext({
          pathname: "/",
          id: "watchlist-cis_1.5_aws",
          framework: "CIS AWS 1.5",
          score: 45,
        }),
        buildServiceSummaryContext({
          pathname: "/",
          service: "s3",
          failedFindingsCount: 34,
          total: 120,
        }),
        buildFilteredProviderContext({
          pathname: "/",
          id: "b81165a0-4f28-4b5c-9a41-1e2d3c4b5a69",
          uid: "123456789012",
          type: "aws",
          alias: "Production",
        }),
        buildProviderGroupContext({
          pathname: "/",
          id: "3f2a1b0c-9d8e-7f60-5a4b-3c2d1e0f9a8b",
          name: "Production accounts",
        }),
      ];

      // When
      const context = compileLighthouseContext(candidates, "overview:/");

      // Then every real Overview contributor fits within the budget
      expect(context).toBeDefined();
      expect(context?.items).toHaveLength(candidates.length);
      expect(context?.items[0]?.kind).toBe("page");
      expect(context?.items.some((item) => item.id.startsWith("group-"))).toBe(
        true,
      );
      expect(getApiLighthouseContextByteLength(context!)).toBeLessThanOrEqual(
        4 * 1024,
      );
    });
  });

  describe("when contributors belong to another page", () => {
    it("should ignore stale scoped data", () => {
      // Given / When
      const context = compileLighthouseContext(
        [
          {
            kind: "page",
            id: "resources",
            source: "automatic",
            scopeKey: "resources:/resources",
            label: "Resources",
            path: "/resources",
          },
          {
            kind: "finding",
            id: "finding-1",
            source: "selection",
            scopeKey: "findings:/findings",
            label: "Old finding",
            findingId: "finding-1",
          },
        ],
        "resources:/resources",
      );

      // Then
      expect(context?.items.map((item) => item.id)).toEqual(["resources"]);
    });
  });

  describe("when current context is invalid", () => {
    it("should return no context so sending remains available", () => {
      // Given / When
      const context = compileLighthouseContext(
        [
          {
            kind: "finding",
            id: "finding-1",
            source: "selection",
            scopeKey: "findings:/findings",
            label: "Invalid finding without findingId",
          },
        ],
        "findings:/findings",
      );

      // Then
      expect(context).toBeUndefined();
    });

    it("should discard valid items together with an invalid same-scope item", () => {
      // Given / When
      const context = compileLighthouseContext(
        [
          {
            kind: "page",
            id: "findings",
            source: "automatic",
            scopeKey: "findings:/findings",
            label: "Findings",
            path: "/findings",
          },
          {
            kind: "finding",
            id: "finding-1",
            source: "selection",
            scopeKey: "findings:/findings",
            label: "Invalid finding without findingId",
          },
        ],
        "findings:/findings",
      );

      // Then
      expect(context).toBeUndefined();
    });
  });
});
