import type { GraphNode } from 'gitnexus-shared';
import { describe, expect, it } from 'vitest';
import {
  decodeSpringAopReason,
  encodeSpringAopReason,
  isSpringAopEvidenceNode,
  parseSpringAopPointcut,
  springAopPointcutMatches,
  type SpringAopReason,
} from '../../src/core/ingestion/frameworks/spring/aop.js';
import {
  createSpringAopCandidateIndex,
  type SpringAopOwnedMethod,
} from '../../src/core/ingestion/frameworks/spring/aop-candidates.js';

const owner: GraphNode = {
  id: 'Class:com.example.service.OrderService',
  label: 'Class',
  properties: {
    name: 'OrderService',
    qualifiedName: 'com.example.service.OrderService',
    filePath: 'src/OrderService.java',
    startLine: 1,
    endLine: 20,
    isExported: true,
  },
};

const method: GraphNode = {
  id: 'Method:com.example.service.OrderService.run',
  label: 'Method',
  properties: {
    name: 'run',
    qualifiedName: 'com.example.service.OrderService.run',
    filePath: 'src/OrderService.java',
    startLine: 3,
    endLine: 5,
    isExported: true,
    visibility: 'public',
    parameterCount: 1,
  },
};

describe('Spring AOP static pointcuts (#2416)', () => {
  it('parses the deliberately narrow execution, within, and known @annotation subset', () => {
    expect(parseSpringAopPointcut('execution(public * com.example..OrderService.r*( * ))')).toEqual(
      {
        kind: 'execution',
        ownerPattern: 'com.example..OrderService',
        methodPattern: 'r*',
        visibility: 'public',
        parameterCount: 1,
      },
    );
    expect(parseSpringAopPointcut('within(com.example..service.*)')).toEqual({
      kind: 'within',
      ownerPattern: 'com.example..service.*',
    });
    expect(
      parseSpringAopPointcut(
        '@annotation(org.springframework.transaction.annotation.Transactional)',
      ),
    ).toEqual({
      kind: 'annotation',
      annotation: 'org.springframework.transaction.annotation.Transactional',
    });
  });

  it('matches owner, method, visibility, arity, and resolved method annotations', () => {
    const execution = parseSpringAopPointcut('execution(public * com.example..OrderService.r*(*))');
    const within = parseSpringAopPointcut('within(com.example..OrderService)');
    const annotation = parseSpringAopPointcut(
      '@annotation(org.springframework.transaction.annotation.Transactional)',
    );

    expect(execution && springAopPointcutMatches(execution, owner, method)).toBe(true);
    expect(within && springAopPointcutMatches(within, owner, method)).toBe(true);
    expect(
      annotation &&
        springAopPointcutMatches(
          annotation,
          owner,
          method,
          new Set(['org.springframework.transaction.annotation.Transactional']),
        ),
    ).toBe(true);
  });

  it('treats an unmodified interface method as public for execution(public ...)', () => {
    const interfaceOwner: GraphNode = { ...owner, label: 'Interface' };
    const implicitPublicMethod: GraphNode = {
      ...method,
      properties: { ...method.properties, visibility: 'package' },
    };
    const execution = parseSpringAopPointcut(
      'execution(public * com.example..OrderService.run(*))',
    );

    expect(
      execution && springAopPointcutMatches(execution, interfaceOwner, implicitPublicMethod),
    ).toBe(true);
    expect(execution && springAopPointcutMatches(execution, owner, implicitPublicMethod)).toBe(
      false,
    );
  });

  it('matches unqualified wildcard type patterns against the owner simple name', () => {
    const within = parseSpringAopPointcut('within(*Service)');
    const execution = parseSpringAopPointcut('execution(* *Service.run(..))');

    expect(within && springAopPointcutMatches(within, owner, method)).toBe(true);
    expect(execution && springAopPointcutMatches(execution, owner, method)).toBe(true);
  });

  it.each(['within(OrderService)', 'execution(* OrderService.run(..))'])(
    'fails closed when a simple exact type needs unavailable import context: %s',
    (expression) => {
      expect(parseSpringAopPointcut(expression)).toBeNull();
    },
  );

  it.each([
    'namedPointcut()',
    'execution(* com.example..OrderService.*(..)) && args(orderId)',
    'execution(String com.example.OrderService.run(..))',
    '@annotation(com.example.DynamicMarker)',
    'bean(orderService)',
  ])('fails closed for unsupported or dynamic expression %s', (expression) => {
    expect(parseSpringAopPointcut(expression)).toBeNull();
  });

  it('rejects overlong input instead of truncating away a dynamic suffix', () => {
    const start = 'execution(* ';
    const end = '.run(..))';
    const validPrefix = `${start}${'a'.repeat(1_000 - start.length - end.length)}${end}`;
    expect(validPrefix).toHaveLength(1_000);
    expect(parseSpringAopPointcut(`${validPrefix} && args(value)`)).toBeNull();
  });

  it('matches adversarial wildcard input without regex backtracking', () => {
    const wildcardChain = '*a'.repeat(10);
    const execution = parseSpringAopPointcut(
      `execution(* com.example..OrderService.${wildcardChain}z(..))`,
    );
    const within = parseSpringAopPointcut(`within(com.example.${wildcardChain}z)`);
    const adversarialMethod = {
      ...method,
      properties: { ...method.properties, name: 'a'.repeat(24) },
    };
    const adversarialOwner = {
      ...owner,
      properties: {
        ...owner.properties,
        qualifiedName: `com.example.${'a'.repeat(24)}`,
      },
    };

    const startedAt = performance.now();
    expect(execution && springAopPointcutMatches(execution, owner, adversarialMethod)).toBe(false);
    expect(within && springAopPointcutMatches(within, adversarialOwner, method)).toBe(false);
    expect(performance.now() - startedAt).toBeLessThan(100);
  });
});

describe('Spring AOP pointcut candidate index (#2416)', () => {
  it('selects only methods that directly declare an @annotation behavior', () => {
    const annotated = { method, owner } satisfies SpringAopOwnedMethod;
    const plain = {
      owner,
      method: {
        ...method,
        id: 'Method:com.example.service.OrderService.plain',
        properties: { ...method.properties, name: 'plain' },
      },
    } satisfies SpringAopOwnedMethod;
    const pointcut = parseSpringAopPointcut(
      '@annotation(org.springframework.transaction.annotation.Transactional)',
    );
    expect(pointcut).not.toBeNull();

    const index = createSpringAopCandidateIndex(
      [annotated, plain],
      new Map([[method.id, new Set(['org.springframework.transaction.annotation.Transactional'])]]),
    );

    expect(index.candidatesFor(pointcut!)).toEqual([annotated]);
  });

  it('narrows exact and literal-prefix owner pointcuts without excluding matches', () => {
    const ownedMethod = (qualifiedName: string): SpringAopOwnedMethod => {
      const ownerName = qualifiedName.slice(qualifiedName.lastIndexOf('.') + 1);
      return {
        owner: {
          ...owner,
          id: `Class:${qualifiedName}`,
          properties: { ...owner.properties, name: ownerName, qualifiedName },
        },
        method: {
          ...method,
          id: `Method:${qualifiedName}.run`,
          properties: { ...method.properties, qualifiedName: `${qualifiedName}.run` },
        },
      };
    };
    const order = ownedMethod('com.example.service.OrderService');
    const payment = ownedMethod('com.example.billing.PaymentService');
    const legacy = ownedMethod('org.legacy.LegacyService');
    const candidates = [order, payment, legacy];
    const index = createSpringAopCandidateIndex(candidates, new Map());
    const exact = parseSpringAopPointcut('within(com.example.service.OrderService)');
    const prefixed = parseSpringAopPointcut('within(com.example..*Service)');
    const leadingWildcard = parseSpringAopPointcut('within(*..*Service)');
    expect(exact && index.candidatesFor(exact)).toEqual([order]);
    expect(prefixed && index.candidatesFor(prefixed)).toEqual([payment, order]);
    expect(leadingWildcard && index.candidatesFor(leadingWildcard)).toEqual(candidates);
  });

  it('preserves brute-force results across exact, wildcard, descendant, and annotation pointcuts', () => {
    const ownedMethod = (
      qualifiedName: string,
      methodName: string,
      filePath: string,
    ): SpringAopOwnedMethod => ({
      owner: {
        ...owner,
        id: `Class:${filePath}:${qualifiedName}`,
        properties: { ...owner.properties, qualifiedName, filePath },
      },
      method: {
        ...method,
        id: `Method:${filePath}:${qualifiedName}.${methodName}`,
        properties: {
          ...method.properties,
          name: methodName,
          qualifiedName: `${qualifiedName}.${methodName}`,
          filePath,
        },
      },
    });
    const candidates = [
      ownedMethod('com.example.service.OrderService', 'run', 'OrderService.java'),
      ownedMethod('com.example.service.OrderService', 'read', 'OrderService.kt'),
      ownedMethod('com.example.billing.InvoiceService', 'run', 'InvoiceService.kt'),
      ownedMethod('org.legacy.LegacyService', 'run', 'LegacyService.java'),
    ];
    const transactional = 'org.springframework.transaction.annotation.Transactional';
    const methodAnnotations = new Map([[candidates[1]!.method.id, new Set([transactional])]]);
    const index = createSpringAopCandidateIndex(candidates, methodAnnotations);
    const expressions = [
      'within(com.example.service.OrderService)',
      'within(com.example..*Service)',
      'within(*..*Service)',
      'execution(public * com.example..*Service.r*( * ))',
      `@annotation(${transactional})`,
    ];

    for (const expression of expressions) {
      const pointcut = parseSpringAopPointcut(expression);
      expect(pointcut, expression).not.toBeNull();
      const matchingIds = (pool: readonly SpringAopOwnedMethod[]) =>
        pool
          .filter((candidate) =>
            springAopPointcutMatches(
              pointcut!,
              candidate.owner,
              candidate.method,
              methodAnnotations.get(candidate.method.id),
            ),
          )
          .map((candidate) => candidate.method.id)
          .sort();

      expect(matchingIds(index.candidatesFor(pointcut!)), expression).toEqual(
        matchingIds(candidates),
      );
    }
  });
});

describe('Spring AOP persisted reason contract (#2416)', () => {
  const reasons: readonly SpringAopReason[] = [
    {
      kind: 'behavior',
      annotation: 'org.springframework.transaction.annotation.Transactional',
      behavior: 'transactional',
      declaredOn: 'method',
      activation: 'unknown',
      proxy: 'possible',
    },
    {
      kind: 'behavior',
      annotation: 'org.springframework.cache.annotation.Caching',
      behavior: 'caching',
      declaredOn: 'method',
      activation: 'unknown',
      proxy: 'possible',
    },
    {
      kind: 'advice',
      annotation: 'org.aspectj.lang.annotation.Around',
      advice: 'around',
      pointcut: 'execution(* com.example..OrderService.*(..))',
      match: 'static',
      activation: 'unknown',
      proxy: 'possible',
    },
    {
      kind: 'pointcut',
      annotation: 'org.aspectj.lang.annotation.Before',
      pointcut: 'namedPointcut()',
      match: 'unresolved',
      resolution: 'unknown',
    },
    {
      kind: 'pointcut',
      annotation: 'org.aspectj.lang.annotation.After',
      pointcut: null,
      match: 'unresolved',
      resolution: 'unknown',
    },
    {
      kind: 'aspect',
      annotation: 'org.aspectj.lang.annotation.Aspect',
      activation: 'unknown',
      registration: 'unknown',
    },
  ];

  it.each(reasons)('round-trips the $kind reason', (reason) => {
    expect(decodeSpringAopReason(encodeSpringAopReason(reason))).toEqual(reason);
  });

  it.each([
    'spring-aop:v2:{}',
    'spring-aop:v1:not-json',
    `spring-aop:v1:${JSON.stringify({
      kind: 'behavior',
      annotation: 'com.example.FakeTransactional',
      behavior: 'transactional',
      declaredOn: 'method',
      activation: 'unknown',
      proxy: 'possible',
    })}`,
    `spring-aop:v1:${JSON.stringify({
      kind: 'pointcut',
      annotation: 'org.aspectj.lang.annotation.Before',
      pointcut: 'execution(* com.example.Service.run(..))',
      match: 'static',
      resolution: 'unknown',
    })}`,
  ])('rejects malformed, foreign, or internally inconsistent reason %s', (reason) => {
    expect(decodeSpringAopReason(reason)).toBeUndefined();
  });

  it('identifies owned evidence by its stable ID namespace, not mutable prose', () => {
    expect(
      isSpringAopEvidenceNode({
        ...method,
        id: 'CodeElement:spring-aop:evidence',
        label: 'CodeElement',
      }),
    ).toBe(true);
    expect(
      isSpringAopEvidenceNode({
        ...method,
        id: 'CodeElement:ordinary',
        label: 'CodeElement',
        properties: { ...method.properties, description: 'Spring AOP: lookalike' },
      }),
    ).toBe(false);
  });
});
