import { beforeEach, describe, expect, it, type Mock, vi } from "vitest";
import type { AccessIntent } from "#src/access-intent/access-intent";
import type { AuthorizerRegistrar } from "#src/authority/authorizer-registry";
import { posixPathFlavor } from "#src/path/path-flavor";
import { PathNormalizer } from "#src/path-normalizer";
import { LocalPermissionsService } from "#src/permissions-service";
import type { ToolAccessExtractorRegistrar } from "#src/tool-access-extractor-registry";
import type {
  ToolInputFormatter,
  ToolInputFormatterRegistrar,
} from "#src/tool-input-formatter-registry";
import type { PermissionCheckResult, PermissionState } from "#src/types";

import { makeCheckResult } from "#test/helpers/handler-fixtures";

// Mock node:fs so realpathSync (the canonical alias) is controllable.
const realpathSync = vi.hoisted(() =>
  vi.fn<(path: string) => string>((p) => p),
);
vi.mock("node:fs", () => ({
  realpathSync,
  default: { realpathSync },
}));

// Mock the advisory bash resolver so the service test asserts delegation; the
// decomposition behavior itself is covered in bash-advisory-check.test.ts.
const resolveBashAdvisoryCheck = vi.hoisted(() =>
  vi.fn<
    (
      command: string,
      agentName: string | undefined,
      resolver: unknown,
    ) => PermissionCheckResult
  >(),
);
vi.mock("#src/bash-advisory-check", () => ({ resolveBashAdvisoryCheck }));

// ── helpers ────────────────────────────────────────────────────────────────

interface FakeResolver {
  resolve: Mock<(intent: AccessIntent) => PermissionCheckResult>;
  getToolPermission: Mock<
    (toolName: string, agentName?: string) => PermissionState
  >;
}

function makeResolver(): FakeResolver {
  return {
    resolve: vi
      .fn<(intent: AccessIntent) => PermissionCheckResult>()
      .mockReturnValue(makeCheckResult()),
    getToolPermission: vi
      .fn<(toolName: string, agentName?: string) => PermissionState>()
      .mockReturnValue("ask"),
  };
}

function makeFormatterRegistry(): ToolInputFormatterRegistrar {
  return {
    register: vi
      .fn<ToolInputFormatterRegistrar["register"]>()
      .mockReturnValue(vi.fn()),
  };
}

function makeAccessExtractorRegistry(): ToolAccessExtractorRegistrar {
  return {
    register: vi
      .fn<ToolAccessExtractorRegistrar["register"]>()
      .mockReturnValue(vi.fn()),
  };
}

function makeAuthorizerRegistry(): AuthorizerRegistrar {
  return {
    register: vi.fn<AuthorizerRegistrar["register"]>().mockReturnValue(vi.fn()),
  };
}

function makeService(overrides?: {
  resolver?: FakeResolver;
  formatterRegistry?: ToolInputFormatterRegistrar;
  accessExtractorRegistry?: ToolAccessExtractorRegistrar;
  authorizerRegistry?: AuthorizerRegistrar;
}) {
  const resolver = overrides?.resolver ?? makeResolver();
  // The published service always answers against the parent session's cwd.
  const session = { getPathNormalizer: () => normalizer };
  const formatterRegistry =
    overrides?.formatterRegistry ?? makeFormatterRegistry();
  const accessExtractorRegistry =
    overrides?.accessExtractorRegistry ?? makeAccessExtractorRegistry();
  const authorizerRegistry =
    overrides?.authorizerRegistry ?? makeAuthorizerRegistry();
  const service = new LocalPermissionsService(
    resolver,
    session,
    formatterRegistry,
    accessExtractorRegistry,
    authorizerRegistry,
  );
  return {
    service,
    resolver,
    formatterRegistry,
    accessExtractorRegistry,
    authorizerRegistry,
  };
}

const normalizer = new PathNormalizer(posixPathFlavor, "/test/project");

// ── tests ──────────────────────────────────────────────────────────────────

beforeEach(() => {
  realpathSync.mockReset();
  realpathSync.mockImplementation((p: string) => p);
  resolveBashAdvisoryCheck.mockReset();
  resolveBashAdvisoryCheck.mockReturnValue(
    makeCheckResult({ toolName: "bash" }),
  );
});

describe("checkPermission", () => {
  it("resolves a non-path surface through a tool intent", () => {
    const { service, resolver } = makeService();
    service.checkPermission("skill", "my-skill", "my-agent");
    expect(resolver.resolve).toHaveBeenCalledWith({
      kind: "tool",
      surface: "skill",
      input: { name: "my-skill" },
      agentName: "my-agent",
    });
  });

  it("routes a bash query through the advisory decomposition resolver", () => {
    const { service, resolver } = makeService();
    const expected = makeCheckResult({ state: "deny", toolName: "bash" });
    resolveBashAdvisoryCheck.mockReturnValue(expected);
    const result = service.checkPermission(
      "bash",
      "cd /repo && npm install x",
      "my-agent",
    );
    expect(resolveBashAdvisoryCheck).toHaveBeenCalledWith(
      "cd /repo && npm install x",
      "my-agent",
      resolver,
    );
    expect(result).toBe(expected);
  });

  it("passes an empty string to the advisory resolver for a value-less bash query", () => {
    const { service, resolver } = makeService();
    service.checkPermission("bash");
    expect(resolveBashAdvisoryCheck).toHaveBeenCalledWith(
      "",
      undefined,
      resolver,
    );
  });

  it("resolves an external_directory path query through an access-path intent matching the canonical alias", () => {
    realpathSync.mockImplementation((p: string) =>
      p === "/test/project/link" ? "/test/project/real" : p,
    );
    const { service, resolver } = makeService();
    service.checkPermission("external_directory", "link");
    expect(resolver.resolve).toHaveBeenCalledTimes(1);
    const intent = resolver.resolve.mock.calls[0][0];
    expect(intent.kind).toBe("access-path");
    if (intent.kind === "access-path") {
      expect(intent.surface).toBe("external_directory");
      expect(intent.path.matchValues()).toContain("/test/project/real");
    }
  });

  it("resolves a path-bearing tool query (read) through an access-path intent", () => {
    const { service, resolver } = makeService();
    service.checkPermission("read", "/test/project/.env");
    const intent = resolver.resolve.mock.calls[0][0];
    expect(intent.kind).toBe("access-path");
    if (intent.kind === "access-path") {
      expect(intent.surface).toBe("read");
      expect(intent.path.value()).toBe("/test/project/.env");
    }
  });

  it("falls back to a tool intent for a value-less path query", () => {
    const { service, resolver } = makeService();
    service.checkPermission("path");
    const intent = resolver.resolve.mock.calls[0][0];
    expect(intent.kind).toBe("tool");
  });

  it("returns the result from resolver.resolve", () => {
    const expected = makeCheckResult({ state: "deny", toolName: "skill" });
    const resolver = makeResolver();
    resolver.resolve.mockReturnValue(expected);
    const { service } = makeService({ resolver });
    const result = service.checkPermission("skill", "my-skill");
    expect(result).toBe(expected);
  });
});

describe("getToolPermission", () => {
  it("delegates to resolver.getToolPermission", () => {
    const resolver = makeResolver();
    resolver.getToolPermission.mockReturnValue("deny");
    const { service } = makeService({ resolver });
    const result = service.getToolPermission("write", "my-agent");
    expect(resolver.getToolPermission).toHaveBeenCalledWith(
      "write",
      "my-agent",
    );
    expect(result).toBe("deny");
  });

  it("omits agentName when not provided", () => {
    const { service, resolver } = makeService();
    service.getToolPermission("read");
    expect(resolver.getToolPermission).toHaveBeenCalledWith("read", undefined);
  });
});

describe("registerToolInputFormatter", () => {
  it("delegates to formatterRegistry.register and returns the unsubscribe function", () => {
    const unsub = vi.fn();
    const { service, formatterRegistry } = makeService();
    vi.mocked(formatterRegistry.register).mockReturnValue(unsub);
    const formatter: ToolInputFormatter = vi.fn();
    const result = service.registerToolInputFormatter("my-tool", formatter);
    expect(formatterRegistry.register).toHaveBeenCalledWith(
      "my-tool",
      formatter,
    );
    expect(result).toBe(unsub);
  });
});

describe("registerToolAccessExtractor", () => {
  it("delegates to accessExtractorRegistry.register and returns the unsubscribe function", () => {
    const unsub = vi.fn();
    const { service, accessExtractorRegistry } = makeService();
    vi.mocked(accessExtractorRegistry.register).mockReturnValue(unsub);
    const extractor = vi.fn();
    const result = service.registerToolAccessExtractor("ffgrep", extractor);
    expect(accessExtractorRegistry.register).toHaveBeenCalledWith(
      "ffgrep",
      extractor,
    );
    expect(result).toBe(unsub);
  });
});

describe("registerAuthorizer", () => {
  it("delegates to authorizerRegistry.register and returns the unsubscribe function", () => {
    const unsub = vi.fn();
    const { service, authorizerRegistry } = makeService();
    vi.mocked(authorizerRegistry.register).mockReturnValue(unsub);
    const authorize = vi.fn();
    const result = service.registerAuthorizer("model-judge", authorize);
    expect(authorizerRegistry.register).toHaveBeenCalledWith(
      "model-judge",
      authorize,
    );
    expect(result).toBe(unsub);
  });
});
