import fs from 'node:fs/promises';
import path from 'node:path';
import { expect, it } from 'vitest';
import { buildTestGraph } from '../helpers/test-graph.js';
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
import { streamAllCSVsToDisk } from '../../src/core/lbug/csv-generator.js';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';

const CLASS_ID = 'Class:src/BillingService.java:BillingService';
const FRAMEWORK_MARKER = 'com.acme.FrameworkMarker';
const itLbugReopen = process.platform === 'win32' ? it.skip : it;

withTestLbugDB('spring-bean-metadata-roundtrip', (handle) => {
  it('preserves Class framework annotations through all write paths', async () => {
    const adapter = await import('../../src/core/lbug/lbug-adapter.js');
    const graph = buildTestGraph([
      {
        id: CLASS_ID,
        label: 'Class',
        name: 'BillingService',
        filePath: 'src/BillingService.java',
        extra: {
          frameworkAnnotations: ['org.springframework.stereotype.Service', FRAMEWORK_MARKER],
        },
      },
    ]);

    const csvDir = path.join(handle.tmpHandle.dbPath, 'csv-spring-bean');
    const repoDir = path.join(handle.tmpHandle.dbPath, 'repo-spring-bean');
    await fs.mkdir(repoDir, { recursive: true });
    await streamAllCSVsToDisk(graph, repoDir, csvDir);

    const classCsvPath = path.join(csvDir, 'class.csv');
    const classCsv = await fs.readFile(classCsvPath, 'utf8');
    expect(classCsv.split('\n')[0]).toBe(
      'id,name,filePath,startLine,endLine,isExported,content,description,frameworkAnnotations',
    );
    expect(classCsv).toContain('org.springframework.stereotype.Service');
    expect(classCsv).toContain(FRAMEWORK_MARKER);

    await adapter.executeQuery(adapter.getCopyQuery('Class', classCsvPath.replace(/\\/g, '/')));
    expect(
      await adapter.executeQuery(
        `MATCH (c:Class {id: '${CLASS_ID}'}) RETURN c.frameworkAnnotations AS frameworkAnnotations`,
      ),
    ).toEqual([
      {
        frameworkAnnotations: ['org.springframework.stereotype.Service', FRAMEWORK_MARKER],
      },
    ]);

    expect(
      await adapter.insertNodeToLbug('Class', {
        id: 'Class:src/Widget.java:Widget',
        name: 'Widget',
        filePath: 'src/Widget.java',
        frameworkAnnotations: ['org.springframework.stereotype.Component'],
      }),
    ).toBe(true);
    expect(
      await adapter.executeQuery(
        `MATCH (c:Class {id: 'Class:src/Widget.java:Widget'}) RETURN c.frameworkAnnotations AS frameworkAnnotations`,
      ),
    ).toEqual([
      {
        frameworkAnnotations: ['org.springframework.stereotype.Component'],
      },
    ]);
  });

  itLbugReopen('preserves Class framework annotations through batch upserts', async () => {
    const adapter = await import('../../src/core/lbug/lbug-adapter.js');
    // The batch helper owns its connection, so release the singleton lock for
    // the call and restore it before the shared fixture tears down.
    await adapter.closeLbug();
    let upsertResult: { inserted: number; failed: number };
    try {
      upsertResult = await adapter.batchInsertNodesToLbug(
        [
          {
            label: 'Class',
            properties: {
              id: CLASS_ID,
              name: 'BillingService',
              filePath: 'src/BillingService.java',
              frameworkAnnotations: ['org.springframework.stereotype.Repository', FRAMEWORK_MARKER],
            },
          },
        ],
        handle.dbPath,
      );
    } finally {
      await adapter.initLbug(handle.dbPath);
    }

    expect(upsertResult).toEqual({ inserted: 1, failed: 0 });
    expect(
      await adapter.executeQuery(
        `MATCH (c:Class {id: '${CLASS_ID}'}) RETURN c.frameworkAnnotations AS frameworkAnnotations`,
      ),
    ).toEqual([
      {
        frameworkAnnotations: ['org.springframework.stereotype.Repository', FRAMEWORK_MARKER],
      },
    ]);
  });

  it('rejects framework annotation items that COPY cannot encode losslessly', async () => {
    const graph = buildTestGraph([
      {
        id: 'Class:src/Unsafe.java:Unsafe',
        label: 'Class',
        name: 'Unsafe',
        filePath: 'src/Unsafe.java',
        extra: { frameworkAnnotations: ['com.acme.Has,Comma'] },
      },
    ]);
    const csvDir = path.join(handle.tmpHandle.dbPath, 'csv-unsafe-framework-annotation');
    const repoDir = path.join(handle.tmpHandle.dbPath, 'repo-unsafe-framework-annotation');
    await fs.mkdir(repoDir, { recursive: true });

    await expect(streamAllCSVsToDisk(graph, repoDir, csvDir)).rejects.toThrow(
      'Cannot safely encode CSV string-list item',
    );
  });

  it('persists pipeline-produced Class and Method INJECTS edges to Bean declarations', async () => {
    const adapter = await import('../../src/core/lbug/lbug-adapter.js');
    const root = path.join(handle.tmpHandle.dbPath, 'spring-bean-injects-roundtrip');
    const repoDir = path.join(root, 'repo');
    const storageDir = path.join(root, 'storage');
    await fs.mkdir(repoDir, { recursive: true });
    await fs.mkdir(storageDir, { recursive: true });
    await Promise.all([
      fs.writeFile(
        path.join(repoDir, 'Gateway.java'),
        `package com.persisted;
public interface Gateway {}
`,
      ),
      fs.writeFile(
        path.join(repoDir, 'DefaultGateway.java'),
        `package com.persisted;
public class DefaultGateway implements Gateway {}
`,
      ),
      fs.writeFile(
        path.join(repoDir, 'PersistedConfig.java'),
        `package com.persisted;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PersistedConfig {
  @Bean Gateway persistedGateway() { return new DefaultGateway(); }
  @Bean Object persistedAggregate(Gateway gateway) { return new Object(); }
}
`,
      ),
      fs.writeFile(
        path.join(repoDir, 'PersistedConsumer.java'),
        `package com.persisted;
import jakarta.annotation.Resource;
public class PersistedConsumer {
  @Resource(name = "persistedGateway") Gateway selected;
}
`,
      ),
    ]);

    const { graph } = await runPipelineFromRepo(repoDir, () => {}, {});
    expect(
      graph.relationships.some(
        (relationship) =>
          relationship.type === 'INJECTS' &&
          graph.getNode(relationship.sourceId)?.properties.name === 'PersistedConsumer' &&
          graph.getNode(relationship.targetId)?.properties.name === 'persistedGateway',
      ),
    ).toBe(true);
    expect(
      graph.relationships.some(
        (relationship) =>
          relationship.type === 'INJECTS' &&
          graph.getNode(relationship.sourceId)?.properties.name === 'persistedAggregate' &&
          graph.getNode(relationship.targetId)?.properties.name === 'persistedGateway',
      ),
    ).toBe(true);

    await adapter.loadGraphToLbug(graph, repoDir, storageDir);

    expect(
      await adapter.executeQuery(
        `MATCH (source:Class)-[r:CodeRelation]->(target:CodeElement)
         WHERE r.type = 'INJECTS'
           AND source.name = 'PersistedConsumer'
           AND target.name = 'persistedGateway'
         RETURN source.name AS source, target.name AS target`,
      ),
    ).toEqual([{ source: 'PersistedConsumer', target: 'persistedGateway' }]);
    expect(
      await adapter.executeQuery(
        `MATCH (source:Method)-[r:CodeRelation]->(target:CodeElement)
         WHERE r.type = 'INJECTS'
           AND source.name = 'persistedAggregate'
           AND target.name = 'persistedGateway'
         RETURN source.name AS source, target.name AS target`,
      ),
    ).toEqual([{ source: 'persistedAggregate', target: 'persistedGateway' }]);
  }, 90_000);
});
