import { Component, Injector, signal } from "@angular/core";
import { TestBed } from "@angular/core/testing";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { z } from "zod";
import { CopilotKit } from "./copilotkit";
import { provideCopilotKit } from "./config";
import { HumanInTheLoop } from "./human-in-the-loop";

const mockSubscribe = vi.fn();
const mockAddTool = vi.fn();
const mockRemoveTool = vi.fn();
const mockSetRuntimeUrl = vi.fn();
const mockSetRuntimeTransport = vi.fn();
const mockSetHeaders = vi.fn();
const mockSetProperties = vi.fn();
const mockSetAgents = vi.fn();
const mockGetAgent = vi.fn();

const licenseKey = "ck_pub_" + "a".repeat(32);

let lastCoreInstance: any;
let lastCoreConfig: any;

vi.mock("@copilotkit/core", () => {
  const CopilotKitCoreRuntimeConnectionStatus = {
    Disconnected: "disconnected",
    Connected: "connected",
    Connecting: "connecting",
    Error: "error",
  } as const;

  class MockCopilotKitCore {
    readonly subscribe = mockSubscribe;
    readonly addTool = mockAddTool;
    readonly removeTool = mockRemoveTool;
    readonly setRuntimeUrl = mockSetRuntimeUrl;
    readonly setRuntimeTransport = mockSetRuntimeTransport;
    readonly setHeaders = mockSetHeaders;
    readonly setProperties = mockSetProperties;
    readonly setAgents__unsafe_dev_only = mockSetAgents;
    readonly getAgent = mockGetAgent;
    agents: Record<string, any> = {};
    runtimeUrl = undefined;
    runtimeTransport = "auto";
    headers: Record<string, string> = {};
    runtimeConnectionStatus =
      CopilotKitCoreRuntimeConnectionStatus.Disconnected;
    listener?: Parameters<typeof mockSubscribe>[0];

    constructor(config: any) {
      lastCoreConfig = config;
      lastCoreInstance = this;
      mockSubscribe.mockImplementationOnce((listener: any) => {
        this.listener = listener;
        return { unsubscribe: vi.fn() };
      });
    }
  }

  return {
    CopilotKitCore: MockCopilotKitCore,
    CopilotKitCoreRuntimeConnectionStatus,
  } as any;
});

describe("CopilotKit", () => {
  beforeEach(() => {
    TestBed.resetTestingModule();
    vi.clearAllMocks();
    document.getElementById("copilotkit-license-watermark")?.remove();
    (globalThis as any).__copilotkitAngularLicenseWatermarkLogged = undefined;
  });

  it("initialises core with transformed tool and renderer config", () => {
    @Component({
      standalone: true,
      selector: "dummy-tool",
      template: "",
    })
    class DummyToolComponent {
      toolCall = signal({} as any);
    }

    TestBed.configureTestingModule({
      providers: [
        provideCopilotKit({
          runtimeUrl: "https://runtime.local",
          headers: { Authorization: "token" },
          properties: { region: "eu" },
          licenseKey,
          tools: [
            {
              name: "search",
              description: "Search something",
              parameters: z.object({ query: z.string() }),
              handler: async () => "done",
              renderer: DummyToolComponent,
            },
          ],
          renderToolCalls: [
            {
              name: "custom",
              args: z.object({ query: z.string() }),
              component: DummyToolComponent,
              agentId: "agent-a",
            },
          ],
        }),
      ],
    });

    const copilotKit = TestBed.inject(CopilotKit);

    expect(lastCoreConfig.runtimeUrl).toBe("https://runtime.local");
    expect(lastCoreConfig.headers).toEqual({
      Authorization: "token",
      "X-CopilotCloud-Public-Api-Key": licenseKey,
    });
    expect(lastCoreConfig.tools).toEqual([
      expect.objectContaining({
        name: "search",
        description: "Search something",
        parameters: expect.anything(),
        handler: expect.any(Function),
        renderer: DummyToolComponent,
      }),
    ]);

    expect(copilotKit.toolCallRenderConfigs()).toEqual([
      {
        name: "custom",
        args: expect.anything(),
        component: DummyToolComponent,
        agentId: "agent-a",
      },
      {
        name: "search",
        args: expect.anything(),
        component: DummyToolComponent,
        agentId: undefined,
      },
    ]);
  });

  it("tracks client tools and executes handlers within injection context", async () => {
    const handlerSpy = vi.fn().mockResolvedValue("handled");
    TestBed.configureTestingModule({
      providers: [provideCopilotKit({ licenseKey })],
    });

    const copilotKit = TestBed.inject(CopilotKit);
    const injector = TestBed.inject(Injector);

    copilotKit.addFrontendTool({
      name: "client",
      description: "Client tool",
      args: z.object({ value: z.string() }),
      component: class {
        toolCall = signal({} as any);
      },
      handler: handlerSpy,
      injector,
    });

    expect(mockAddTool).toHaveBeenCalledWith(
      expect.objectContaining({ name: "client" }),
    );
    expect(copilotKit.clientToolCallRenderConfigs()).toHaveLength(1);

    const tool = mockAddTool.mock.calls.at(-1)![0];
    const mockAgent = { agentId: "test-agent" };
    const mockContext = {
      toolCall: { id: "call-1", function: { name: "client" } },
      agent: mockAgent,
    };
    await tool.handler({ value: "ok" }, mockContext);
    expect(handlerSpy).toHaveBeenCalledWith({ value: "ok" }, mockContext);
  });

  it("registers human-in-the-loop tools and delegates responses", async () => {
    const onResultSpy = vi
      .spyOn(HumanInTheLoop.prototype, "onResult")
      .mockResolvedValue("result");

    TestBed.configureTestingModule({
      providers: [provideCopilotKit({ licenseKey })],
    });

    const copilotKit = TestBed.inject(CopilotKit);

    const toolConfig = {
      name: "approval",
      args: z.object({ summary: z.string() }),
      component: class {
        toolCall = signal({} as any);
      },
      toolCall: vi.fn(),
      agentId: "agent-1",
    } as const;

    copilotKit.addHumanInTheLoop(toolConfig);

    expect(copilotKit.humanInTheLoopToolRenderConfigs()).toEqual([toolConfig]);
    expect(mockAddTool).toHaveBeenCalledWith(
      expect.objectContaining({ name: "approval" }),
    );

    const tool = mockAddTool.mock.calls.at(-1)![0];
    const mockAgent = { agentId: "agent-1" };
    await tool.handler(
      {},
      {
        toolCall: { id: "call-1", function: { name: "approval" } },
        agent: mockAgent,
      },
    );
    expect(onResultSpy).toHaveBeenCalledWith("call-1", "approval");

    onResultSpy.mockRestore();
  });

  it("removes tools and renderer configs", () => {
    TestBed.configureTestingModule({
      providers: [provideCopilotKit({ licenseKey })],
    });

    const copilotKit = TestBed.inject(CopilotKit);

    copilotKit.addRenderToolCall({
      name: "temp",
      args: z.object({}),
      component: class {
        toolCall = signal({} as any);
      },
      agentId: undefined,
    });

    copilotKit.removeTool("temp");

    expect(mockRemoveTool).toHaveBeenCalledWith("temp", undefined);
    expect(copilotKit.toolCallRenderConfigs()).toEqual([]);
  });

  it("updates runtime configuration via core methods", () => {
    TestBed.configureTestingModule({
      providers: [provideCopilotKit({ licenseKey })],
    });

    const copilotKit = TestBed.inject(CopilotKit);

    copilotKit.updateRuntime({
      runtimeUrl: "https://other",
      runtimeTransport: "single",
      headers: { Authorization: "different" },
      properties: { locale: "en" },
      agents: { a: {} as any },
    });

    expect(mockSetRuntimeUrl).toHaveBeenCalledWith("https://other");
    expect(mockSetRuntimeTransport).toHaveBeenCalledWith("single");
    expect(mockSetHeaders).toHaveBeenCalledWith({ Authorization: "different" });
    expect(mockSetProperties).toHaveBeenCalledWith({ locale: "en" });
    expect(mockSetAgents).toHaveBeenCalledWith({ a: {} });
  });

  it("reflects agent updates from core subscriptions", () => {
    TestBed.configureTestingModule({
      providers: [provideCopilotKit({ licenseKey })],
    });

    const copilotKit = TestBed.inject(CopilotKit);
    const core = lastCoreInstance!;

    core.agents = {
      agent1: { id: "agent1" },
    } as any;

    core.listener!.onAgentsChanged();
    expect(copilotKit.agents()).toEqual(core.agents);
  });

  it("adds app watermark when license key is missing", () => {
    TestBed.configureTestingModule({
      providers: [provideCopilotKit({ runtimeUrl: "https://runtime.local" })],
    });

    TestBed.inject(CopilotKit);

    expect(
      document.getElementById("copilotkit-license-watermark"),
    ).toBeTruthy();
  });
});
