import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'node:fs';
import fsp from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';

const { parseSourceSafeSpy } = vi.hoisted(() => ({ parseSourceSafeSpy: vi.fn() }));

vi.mock('../../../src/core/tree-sitter/safe-parse.js', async () => {
  const { buildSafeParseMock } = await import('../../helpers/parse-source-safe-mock.js');
  return buildSafeParseMock(parseSourceSafeSpy);
});

import {
  ThriftExtractor,
  buildThriftContext,
  thriftMethodContractId,
  thriftServiceContractId,
} from '../../../src/core/group/extractors/thrift-extractor.js';
import type { RepoHandle } from '../../../src/core/group/types.js';

describe('ThriftExtractor', () => {
  let tmpDir: string;
  let extractor: ThriftExtractor;

  beforeEach(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'gitnexus-thrift-'));
    extractor = new ThriftExtractor();
  });

  afterEach(async () => {
    await fsp.rm(tmpDir, { recursive: true, force: true });
  });

  function writeFile(relPath: string, content: string): void {
    const full = path.join(tmpDir, relPath);
    fs.mkdirSync(path.dirname(full), { recursive: true });
    fs.writeFileSync(full, content);
  }

  const makeRepo = (repoPath: string): RepoHandle => ({
    id: 'test-repo',
    path: 'test/app',
    repoPath,
    storagePath: path.join(repoPath, '.gitnexus'),
  });

  it('test_extract_thrift_single_method_returns_idl_provider', async () => {
    writeFile(
      'idl/order.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));

    expect(contracts).toHaveLength(1);
    expect(contracts[0]).toMatchObject({
      contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
      type: 'thrift',
      role: 'provider',
      symbolName: 'OrderService.PlaceOrder',
      confidence: 0.85,
      meta: {
        namespace: 'billing.v1',
        service: 'OrderService',
        method: 'PlaceOrder',
        source: 'thrift_idl',
      },
    });
    expect(contracts[0].symbolRef).toEqual({
      filePath: 'idl/order.thrift',
      name: 'OrderService.PlaceOrder',
    });
  });

  it('test_extract_thrift_multiple_services_and_methods_returns_all', async () => {
    writeFile(
      'contracts/orders.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
  OrderStatus GetOrderStatus(1: string orderId)
}

service InvoiceService {
  Invoice CreateInvoice(1: string orderId)
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));

    expect(contracts.map((c) => c.contractId).sort()).toEqual([
      'thrift::billing.v1.InvoiceService/CreateInvoice',
      'thrift::billing.v1.OrderService/GetOrderStatus',
      'thrift::billing.v1.OrderService/PlaceOrder',
    ]);
  });

  it('test_extract_thrift_prefers_java_namespace_over_other_namespaces', async () => {
    writeFile(
      'order.thrift',
      `namespace py billing_python.v1
namespace java billing.v1
namespace go billinggo

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));

    expect(contracts[0].contractId).toBe('thrift::billing.v1.OrderService/PlaceOrder');
    expect(contracts[0].meta.namespace).toBe('billing.v1');
  });

  it('test_extract_thrift_uses_first_non_java_namespace_when_java_missing', async () => {
    writeFile(
      'order.thrift',
      `namespace py billing_python.v1
namespace go billinggo

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));

    expect(contracts[0].contractId).toBe('thrift::billing_python.v1.OrderService/PlaceOrder');
    expect(contracts[0].meta.namespace).toBe('billing_python.v1');
  });

  it('test_extract_thrift_without_namespace_uses_service_only', async () => {
    writeFile(
      'order.thrift',
      `service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));

    expect(contracts[0].contractId).toBe('thrift::OrderService/PlaceOrder');
    expect(contracts[0].meta.namespace).toBe('');
  });

  it('test_extract_thrift_ignores_braces_inside_comments_and_strings', async () => {
    writeFile(
      'idl/tricky.thrift',
      `namespace java billing.v1

service OrderService {
  // A comment with } should not close the service.
  /* A block comment with { and } should not affect depth. */
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
  const string NOTE = "literal with } and { braces"
  OrderStatus GetOrderStatus(1: string orderId)
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));

    expect(contracts.map((c) => c.symbolName).sort()).toEqual([
      'OrderService.GetOrderStatus',
      'OrderService.PlaceOrder',
    ]);
  });

  it('test_extract_thrift_malformed_unclosed_service_is_skipped', async () => {
    writeFile(
      'idl/broken.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
`,
    );

    await expect(extractor.extract(null, tmpDir, makeRepo(tmpDir))).resolves.toEqual([]);
  });

  it('test_extract_repo_without_thrift_returns_empty', async () => {
    writeFile('src/index.ts', 'console.log("hello")');

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));

    expect(contracts).toEqual([]);
  });

  it('test_extract_java_thrift_consumers_from_iface_client_and_service_fields', async () => {
    writeFile(
      'idl/order.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );
    writeFile(
      'src/main/java/example/BillingWorkflow.java',
      `package example;

class BillingWorkflow {
  private OrderService.Iface orderService;
  private OrderService.Client orderClient;
  private OrderService generatedOrderService;

  void submit(PlaceOrderRequest request) throws Exception {
    orderService.PlaceOrder(request);
    orderClient.PlaceOrder(request);
    generatedOrderService.PlaceOrder(request);
  }
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
    const consumers = contracts
      .filter((c) => c.role === 'consumer')
      .sort((a, b) => a.symbolName.localeCompare(b.symbolName));

    expect(consumers).toHaveLength(3);
    expect(consumers.map((c) => c.symbolName)).toEqual([
      'generatedOrderService.PlaceOrder',
      'orderClient.PlaceOrder',
      'orderService.PlaceOrder',
    ]);
    for (const contract of consumers) {
      expect(contract).toMatchObject({
        contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
        type: 'thrift',
        role: 'consumer',
        confidence: 0.75,
        meta: {
          namespace: 'billing.v1',
          service: 'OrderService',
          method: 'PlaceOrder',
          source: 'java_thrift_consumer',
        },
      });
      expect(contract.symbolRef.filePath).toBe('src/main/java/example/BillingWorkflow.java');
    }
  });

  it('test_extract_java_thrift_consumers_from_this_field_access', async () => {
    writeFile(
      'idl/order.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );
    writeFile(
      'src/main/java/example/BillingWorkflow.java',
      `package example;

class BillingWorkflow {
  private OrderService.Client orderClient;

  void submit(PlaceOrderRequest request) throws Exception {
    this.orderClient.PlaceOrder(request);
  }
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
    const consumers = contracts.filter((c) => c.role === 'consumer');

    expect(consumers).toHaveLength(1);
    expect(consumers[0]).toMatchObject({
      contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
      type: 'thrift',
      role: 'consumer',
      symbolName: 'orderClient.PlaceOrder',
      confidence: 0.75,
      meta: {
        namespace: 'billing.v1',
        service: 'OrderService',
        method: 'PlaceOrder',
        source: 'java_thrift_consumer',
      },
    });
  });

  it('test_extract_java_thrift_consumers_from_fully_qualified_generated_types', async () => {
    writeFile(
      'idl/order.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );
    writeFile(
      'src/main/java/example/BillingWorkflow.java',
      `package example;

class BillingWorkflow {
  private billing.v1.OrderService.Iface orderService;
  private billing.v1.OrderService.Client orderClient;

  void submit(PlaceOrderRequest request) throws Exception {
    orderService.PlaceOrder(request);
    orderClient.PlaceOrder(request);
  }
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
    const consumers = contracts
      .filter((c) => c.role === 'consumer')
      .sort((a, b) => a.symbolName.localeCompare(b.symbolName));

    expect(consumers).toHaveLength(2);
    expect(consumers.map((c) => c.symbolName)).toEqual([
      'orderClient.PlaceOrder',
      'orderService.PlaceOrder',
    ]);
    expect(new Set(consumers.map((c) => c.contractId))).toEqual(
      new Set(['thrift::billing.v1.OrderService/PlaceOrder']),
    );
  });

  it('test_extract_java_thrift_consumers_from_local_variables', async () => {
    writeFile(
      'idl/order.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );
    writeFile(
      'src/main/java/example/BillingWorker.java',
      `package example;

class BillingWorker {
  void submit(OrderService.Iface iface, OrderService.Client client, OrderService service) throws Exception {
    OrderService.Iface orderService = iface;
    OrderService.Client orderClient = client;
    OrderService generatedOrderService = service;

    orderService.PlaceOrder(new PlaceOrderRequest());
    orderClient.PlaceOrder(new PlaceOrderRequest());
    generatedOrderService.PlaceOrder(new PlaceOrderRequest());
  }
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
    const consumers = contracts.filter((c) => c.role === 'consumer');

    expect(consumers.map((c) => c.symbolName).sort()).toEqual([
      'generatedOrderService.PlaceOrder',
      'orderClient.PlaceOrder',
      'orderService.PlaceOrder',
    ]);
    expect(new Set(consumers.map((c) => c.contractId))).toEqual(
      new Set(['thrift::billing.v1.OrderService/PlaceOrder']),
    );
  });

  it('test_extract_java_thrift_consumers_resolve_receiver_by_nearest_scope', async () => {
    writeFile(
      'idl/order.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}

service InvoiceService {
  Invoice CreateInvoice(1: string orderId)
}`,
    );
    writeFile(
      'src/main/java/example/BillingWorker.java',
      `package example;

class BillingWorker {
  void submitOrder(OrderService.Iface client, PlaceOrderRequest request) throws Exception {
    client.PlaceOrder(request);
  }

  void submitInvoice() throws Exception {
    InvoiceService.Client client = new InvoiceService.Client(null);
    client.CreateInvoice("order-1");
  }
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
    const consumers = contracts
      .filter((c) => c.role === 'consumer')
      .sort((a, b) => a.contractId.localeCompare(b.contractId));

    expect(consumers.map((c) => c.contractId)).toEqual([
      'thrift::billing.v1.InvoiceService/CreateInvoice',
      'thrift::billing.v1.OrderService/PlaceOrder',
    ]);
    expect(consumers.map((c) => c.symbolName).sort()).toEqual([
      'client.CreateInvoice',
      'client.PlaceOrder',
    ]);
  });

  it('test_extract_java_thrift_providers_from_iface_and_service_implements', async () => {
    writeFile(
      'idl/order.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );
    writeFile(
      'src/main/java/example/IfaceOrderHandler.java',
      `package example;

class IfaceOrderHandler implements OrderService.Iface {
  public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) {
    return new PlaceOrderResponse();
  }
}`,
    );
    writeFile(
      'src/main/java/example/GeneratedOrderHandler.java',
      `package example;

class GeneratedOrderHandler implements OrderService {
  public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) {
    return new PlaceOrderResponse();
  }
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
    const providers = contracts
      .filter((c) => c.meta.source === 'java_thrift_provider')
      .sort((a, b) => a.symbolRef.filePath.localeCompare(b.symbolRef.filePath));

    expect(providers).toHaveLength(2);
    for (const contract of providers) {
      expect(contract).toMatchObject({
        contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
        type: 'thrift',
        role: 'provider',
        symbolName: 'OrderService.PlaceOrder',
        confidence: 0.8,
        meta: {
          namespace: 'billing.v1',
          service: 'OrderService',
          method: 'PlaceOrder',
          source: 'java_thrift_provider',
        },
      });
    }
  });

  it('test_extract_thrift_source_scan_contracts_have_stable_distinct_symbol_uids', async () => {
    writeFile(
      'idl/order.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );
    writeFile(
      'src/main/java/example/IfaceOrderHandler.java',
      `package example;

class IfaceOrderHandler implements OrderService.Iface {
  public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) {
    return new PlaceOrderResponse();
  }
}`,
    );

    const first = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
    const second = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
    const providers = first
      .filter((c) => c.role === 'provider')
      .sort((a, b) => a.symbolRef.filePath.localeCompare(b.symbolRef.filePath));
    const repeatedProviders = second
      .filter((c) => c.role === 'provider')
      .sort((a, b) => a.symbolRef.filePath.localeCompare(b.symbolRef.filePath));

    expect(providers).toHaveLength(2);
    expect(providers.map((c) => c.symbolUid)).toEqual(repeatedProviders.map((c) => c.symbolUid));
    expect(providers.every((c) => c.symbolUid.length > 0)).toBe(true);
    expect(new Set(providers.map((c) => c.symbolUid)).size).toBe(2);
    expect(providers.every((c) => !c.symbolUid.includes('::thrift::billing.v1'))).toBe(true);
  });

  it('test_extract_java_thrift_providers_from_fully_qualified_generated_iface', async () => {
    writeFile(
      'idl/order.thrift',
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
    );
    writeFile(
      'src/main/java/example/IfaceOrderHandler.java',
      `package example;

class IfaceOrderHandler implements billing.v1.OrderService.Iface {
  public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) {
    return new PlaceOrderResponse();
  }
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
    const providers = contracts.filter((c) => c.meta.source === 'java_thrift_provider');

    expect(providers).toHaveLength(1);
    expect(providers[0]).toMatchObject({
      contractId: 'thrift::billing.v1.OrderService/PlaceOrder',
      type: 'thrift',
      role: 'provider',
      symbolName: 'OrderService.PlaceOrder',
      confidence: 0.8,
      meta: {
        namespace: 'billing.v1',
        service: 'OrderService',
        method: 'PlaceOrder',
        source: 'java_thrift_provider',
      },
    });
  });

  it('test_extract_java_thrift_consumer_without_idl_emits_weak_method_contract', async () => {
    writeFile(
      'src/main/java/example/BillingWorkflow.java',
      `package example;

class BillingWorkflow {
  private OrderService.Iface orderService;

  void submit(PlaceOrderRequest request) throws Exception {
    orderService.PlaceOrder(request);
  }
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));

    expect(contracts).toHaveLength(1);
    expect(contracts[0]).toMatchObject({
      contractId: 'thrift::OrderService/PlaceOrder',
      type: 'thrift',
      role: 'consumer',
      symbolName: 'orderService.PlaceOrder',
      confidence: 0.45,
      meta: {
        service: 'OrderService',
        method: 'PlaceOrder',
        source: 'java_thrift_consumer_weak',
      },
    });
    expect(contracts[0].symbolRef.filePath).toBe('src/main/java/example/BillingWorkflow.java');
  });

  it('test_extract_java_thrift_direct_service_consumer_without_idl_returns_empty', async () => {
    writeFile(
      'src/main/java/example/PaymentWorkflow.java',
      `package example;

class PaymentWorkflow {
  private PaymentService paymentService;

  void submit() {
    paymentService.charge();
  }
}`,
    );

    const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));

    expect(contracts).toEqual([]);
  });

  describe('Windows SIGSEGV regression — large input must route through parseSourceSafe', () => {
    it('routes >32 767-char source file through parseSourceSafe (not direct parser.parse)', async () => {
      parseSourceSafeSpy.mockClear();

      // Need a base .thrift file so buildThriftContext finds at least one
      // service to scan; without it the source-scan loop short-circuits.
      writeFile(
        'idl/order.thrift',
        `namespace java billing.v1
service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
}`,
      );

      // >40 000-char Java client file. Direct parser.parse(content) on a
      // string this size SIGSEGVs the process on Windows. The spy assertion
      // catches the regression — a "no throw" assertion alone is satisfied
      // by the bypass on Linux/macOS where parser.parse(40 000 chars) succeeds.
      const padding = Array.from(
        { length: 600 },
        (_, i) => `    public String helper${i}() { return "padding-${i}-aaaaaaaaaaaaaaaaaaa"; }\n`,
      ).join('');
      const largeJava = `package com.example;\n\nimport billing.v1.OrderService;\n\npublic class BigClient {\n  private OrderService.Iface client;\n${padding}}\n`;
      expect(largeJava.length).toBeGreaterThan(40_000);

      writeFile('src/main/java/com/example/BigClient.java', largeJava);

      await extractor.extract(null, tmpDir, makeRepo(tmpDir));

      expect(parseSourceSafeSpy).toHaveBeenCalled();
    });
  });
});

describe('buildThriftContext', () => {
  let tmpDir: string;

  beforeEach(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'gitnexus-thrift-context-'));
  });

  afterEach(async () => {
    await fsp.rm(tmpDir, { recursive: true, force: true });
  });

  it('test_buildThriftContext_parses_namespace_service_methods_and_path', async () => {
    await fsp.mkdir(path.join(tmpDir, 'idl'), { recursive: true });
    await fsp.writeFile(
      path.join(tmpDir, 'idl', 'order.thrift'),
      `namespace java billing.v1

service OrderService {
  PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request)
  OrderStatus GetOrderStatus(1: string orderId)
}`,
    );

    const context = await buildThriftContext(tmpDir);

    expect(context.namespacesByThrift.get('idl/order.thrift')).toBe('billing.v1');
    expect(context.servicesByName.get('OrderService')).toEqual([
      {
        namespace: 'billing.v1',
        serviceName: 'OrderService',
        methods: ['PlaceOrder', 'GetOrderStatus'],
        thriftPath: 'idl/order.thrift',
      },
    ]);
  });

  it('test_buildThriftContext_without_files_returns_empty_maps', async () => {
    const context = await buildThriftContext(tmpDir);

    expect(context.namespacesByThrift.size).toBe(0);
    expect(context.servicesByName.size).toBe(0);
  });
});

describe('Thrift contract id helpers', () => {
  it('test_thriftMethodContractId_with_namespace', () => {
    expect(thriftMethodContractId('billing.v1', 'OrderService', 'PlaceOrder')).toBe(
      'thrift::billing.v1.OrderService/PlaceOrder',
    );
  });

  it('test_thriftMethodContractId_without_namespace', () => {
    expect(thriftMethodContractId('', 'OrderService', 'PlaceOrder')).toBe(
      'thrift::OrderService/PlaceOrder',
    );
  });

  it('test_thriftServiceContractId_with_namespace', () => {
    expect(thriftServiceContractId('billing.v1', 'OrderService')).toBe(
      'thrift::billing.v1.OrderService/*',
    );
  });

  it('test_thriftServiceContractId_without_namespace', () => {
    expect(thriftServiceContractId('', 'OrderService')).toBe('thrift::OrderService/*');
  });
});
