import { applySnippetPatch } from './index';
import * as path from 'path';
import {
  SPECIAL_PATCH_BEGIN_FILE_MARKER,
  SPECIAL_PATCH_END_FILE_MARKER,
} from '../../src/llm-orchestration/parser/parsing.constants';

const TS_WASM_PATH = path.join(
  __dirname,
  './grammar/tree-sitter-typescript.wasm',
);

// Helper to normalize whitespace for robust comparison
const normalize = (str: string) =>
  str
    .replace(/}/g, '} ')
    .replace(/\s+/g, ' ')
    .trim();

const runTest = async (
  sourceCode: string,
  patchCode: string,
  expectedResult: string,
) => {
  const resultTiktoken = await applySnippetPatch(sourceCode, patchCode, {
    useTiktoken: true,
  });
  expect(normalize(resultTiktoken)).toEqual(normalize(expectedResult));

  const resultTreeSitter = await applySnippetPatch(
    sourceCode,
    patchCode,
    TS_WASM_PATH,
  );
  expect(normalize(resultTreeSitter)).toEqual(normalize(expectedResult));
};

describe('applySnippetPatch', () => {
  it('should replace a method body in a class by automatically finding anchor size', async () => {
    const sourceCode = `
class Greeter {
  greet() {
    return "Hello, world";
  }
  
  fc(){ return 42; }
}
`;
    const patchCode = `
  greet() {
    // A new implementation
    return "Hello, TypeScript!";
  }
  fc(){
`;
    const expectedResult = `
class Greeter {
  greet() {
    // A new implementation
    return "Hello, TypeScript!";
  }
  fc(){ return 42; }

}
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });

  it('should replace a data structure definition', async () => {
    const sourceCode = `
interface MyData {
  field1: string;
}
console.log("hello");
`;
    const patchCode = `
interface MyData {
  field1: number;
  field2: boolean;
}
`;
    const expectedResult = `
interface MyData {
  field1: number;
  field2: boolean;
}
console.log("hello");
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });

  it('should replace a full function definition', async () => {
    const sourceCode = `
import fs from 'fs';

function calculate() {
  return 1 + 1;
}

export { calculate };
`;
    const patchCode = `
function calculate() {
  // A more complex calculation
  const result = 2 * 2;
  return result;
}
`;
    const expectedResult = `
import fs from 'fs';

function calculate() {
  // A more complex calculation
  const result = 2 * 2;
  return result;
}

export { calculate };
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });

  it(`should replace a beginning of the file ${SPECIAL_PATCH_BEGIN_FILE_MARKER}`, async () => {
    const sourceCode = `
import { ModuleA } from './moduleA';
import { ModuleB } from './moduleB';

console.log('starting up');
`;
    const patchCode = `
// ${SPECIAL_PATCH_BEGIN_FILE_MARKER}
import groupBy from  'lodash'; 
import { ModuleA } from './moduleA';
`;
    const expectedResult = `
import groupBy from  'lodash'; 
import { ModuleA } from './moduleA';
import { ModuleB } from './moduleB';

console.log('starting up');
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });

  it(`should handle ${SPECIAL_PATCH_BEGIN_FILE_MARKER} marker with variations`, async () => {
    const sourceCode = `
import { ModuleA } from './moduleA';
import { ModuleB } from './moduleB';

console.log('starting up');
`;
    const patchCode = `
//${SPECIAL_PATCH_BEGIN_FILE_MARKER} extra text
import groupBy from  'lodash'; 
import { ModuleA } from './moduleA';
`;
    const expectedResult = `
import groupBy from  'lodash'; 
import { ModuleA } from './moduleA';
import { ModuleB } from './moduleB';

console.log('starting up');
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });

  it(`should replace a end of the file ${SPECIAL_PATCH_END_FILE_MARKER}`, async () => {
    const sourceCode = `
import fs from 'fs';

function calculate() {
  return 1 + 1;
}

export { calculate };
`;
    const patchCode = `
export { calculate };
export { sum };
// ${SPECIAL_PATCH_END_FILE_MARKER}
`;
    const expectedResult = `
import fs from 'fs';

function calculate() {
  return 1 + 1;
}

export { calculate };
export { sum };
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });

  it(`should handle ${SPECIAL_PATCH_END_FILE_MARKER} marker with variations`, async () => {
    const sourceCode = `
import fs from 'fs';

function calculate() {
  return 1 + 1;
}

export { calculate };
`;
    const patchCode = `
export { calculate };
export { sum };
//${SPECIAL_PATCH_END_FILE_MARKER} some extra text
`;
    const expectedResult = `
import fs from 'fs';

function calculate() {
  return 1 + 1;
}

export { calculate };
export { sum };
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });
  it('should succeed with dynamic anchor sizing when initial anchors are ambiguous', async () => {
    const sourceCode = `
import dotenv from 'dotenv';
const config = {
    port: 8080,
    host: 'localhost',
};

function connect() {
    // uses config
}

const config2 = {
    port: 3000,
    host: 'remote',
};
`;
    const patchCode = `
import dotenv from 'dotenv';
const config = {
    port: 9000,
    host: 'localhost',
    protocol: 'https'
};

function connect() {
`;
    const expectedResult = `
import dotenv from 'dotenv';
const config = {
    port: 9000,
    host: 'localhost',
    protocol: 'https'
};

function connect() {
    // uses config
}

const config2 = {
    port: 3000,
    host: 'remote',
};
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });

  it('should handle partial type definitions by trimming ambiguous tokens', async () => {
    const sourceCode = `
    /**
     * Bill date of the vendor bill
     */
    billDate: everest_appserver_primitive_PlainDate;
    billNumber: everest_appserver_primitive_Text;
    businessPartnerId: everest_appserver_primitive_ID;
    /**
     * Conditionally return status or paymentStatus
     */
    combinedStatus?: everest_appserver_primitive_Text | null;
    /**
     * Contains Billable Expense line
     */
    containsBillableItem?: everest_appserver_primitive_TrueFalse | null;
    /**
     * Denotes whether bill type is prepaid or not
     */
    containsPrepaidItem?: everest_appserver_primitive_TrueFalse | null;
`;
    const patchCode = `
    businessPartnerId: everest_appserver_primitive_ID;
    /**
     * Conditionally return status or paymentStatus
     */
    combinedStatusNew2?: everest_appserver_primitive_Text | null;
    /**
     * Contains Billable Expense line
     */
`;
    const expectedResult = `
    /**
     * Bill date of the vendor bill
     */
    billDate: everest_appserver_primitive_PlainDate;
    billNumber: everest_appserver_primitive_Text;
    businessPartnerId: everest_appserver_primitive_ID;
    /**
     * Conditionally return status or paymentStatus
     */
    combinedStatusNew2?: everest_appserver_primitive_Text | null;
    /**
     * Contains Billable Expense line
     */
    containsBillableItem?: everest_appserver_primitive_TrueFalse | null;
    /**
     * Denotes whether bill type is prepaid or not
     */
    containsPrepaidItem?: everest_appserver_primitive_TrueFalse | null;
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });

  it('should patch TSX correctly by trimming tokens from invalid partial snippets', async () => {
    const sourceCode = `
    import { useEffect, useMemo } from "react"
import { trpc } from "@/lib/trpc"

import { Button } from "@/components/ui/button"
import { DataTable } from "@/components/ui/data-table"
import { useReconciliationStore } from "@/store/reconciliation"

import {
  getBillColumns,
  getInvoiceColumns,
  getMatchedColumns,
  getTransactionColumns,
} from "./transactions-columns"

export function TransactionsPage() {
  const {
    transactions,
    invoices,
    matchedItems,
    selectedTransactionId,
    selectedMatchableId,
    loadInitialData,
    selectTransaction,
    selectMatchable,
    matchItems,
    unmatchItems,
  } = useReconciliationStore()
  const { data: bills = [] } = trpc.bills.getBills.useQuery()

  // Load mock data when component mounts
  useEffect(() => {
    loadInitialData()
  }, [loadInitialData])

  const selectedTransaction =
    transactions.find((t) => t.id === selectedTransactionId) || null

  const transactionColumns = useMemo(() => getTransactionColumns(), [])

  const invoiceColumns = useMemo(() => getInvoiceColumns(), [])

  const billColumns = useMemo(() => getBillColumns(), [])

  const matchedColumns = useMemo(
    () =>
      getMatchedColumns({
        onUnmatch: unmatchItems,
      }),
    [unmatchItems]
  )

  const canMatch = selectedTransactionId && selectedMatchableId

  return (
    <div>
      <div className="flex justify-between items-center mb-4">
        <h2 className="text-2xl font-bold">Transaction Matching</h2>
        <Button onClick={matchItems} disabled={!canMatch}>
          Match Items
        </Button>
      </div>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
        {/* Left Panel: Bank Transactions */}
        <div>
          <h3 className="text-lg font-semibold mb-2">Bank Transactions</h3>
          <p className="text-sm text-muted-foreground mb-4">
            Select a transaction to begin matching.
          </p>
          <DataTable
            columns={transactionColumns}
            data={transactions}
            onRowClick={(row) => selectTransaction(row.id)}
            selectedId={selectedTransactionId}
          />
        </div>

        {/* Right Panel: Matching Items */}
        <div>
          <h3 className="text-lg font-semibold mb-2">Potential Matches</h3>
          {!selectedTransaction ? (
            <div className="h-full flex items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/30 p-8">
              <p className="text-muted-foreground">
                Select a transaction to see potential matches.
              </p>
            </div>
          ) : selectedTransaction.amount > 0 ? (
            <div>
              <h4 className="text-md font-semibold mb-2 text-muted-foreground">
                Unpaid Invoices
              </h4>
              <DataTable
                columns={invoiceColumns}
                data={invoices}
                onRowClick={(row) => selectMatchable(row.id)}
                selectedId={selectedMatchableId}
              />
            </div>
          ) : (
            <div>
              <h4 className="text-md font-semibold mb-2 text-muted-foreground">
                Unpaid Bills
              </h4>
              <DataTable
                columns={billColumns}
                data={bills}
                onRowClick={(row) => {
                  console.log(\`[UI] Selected matchable bill ID\`)
                  selectMatchable(row.id)
                }}
                selectedId={selectedMatchableId}
              />
            </div>
          )}
        </div>
      </div>

      {matchedItems.length > 0 && (
        <div className="mt-12">
          <h3 className="text-lg font-semibold mb-2">Matched Transactions</h3>
          <DataTable columns={matchedColumns} data={matchedItems} />
        </div>
      )}
    </div>
  )
}
`;
    const patchCode = `
Unpaid Bills
              </h4>
              <DataTable
                columns={billColumns}
                data={bills}
                onRowClick={(row) => {
                  console.log(\`[UI] Selected matchable bill ID\`)
                  selectMatchable(row.id)
                }}
                selectedId={selectedMatchableId}
              />
            </div>
          )}
`;
    const expectedResult = `
    import { useEffect, useMemo } from "react"
import { trpc } from "@/lib/trpc"

import { Button } from "@/components/ui/button"
import { DataTable } from "@/components/ui/data-table"
import { useReconciliationStore } from "@/store/reconciliation"

import {
  getBillColumns,
  getInvoiceColumns,
  getMatchedColumns,
  getTransactionColumns,
} from "./transactions-columns"

export function TransactionsPage() {
  const {
    transactions,
    invoices,
    matchedItems,
    selectedTransactionId,
    selectedMatchableId,
    loadInitialData,
    selectTransaction,
    selectMatchable,
    matchItems,
    unmatchItems,
  } = useReconciliationStore()
  const { data: bills = [] } = trpc.bills.getBills.useQuery()

  // Load mock data when component mounts
  useEffect(() => {
    loadInitialData()
  }, [loadInitialData])

  const selectedTransaction =
    transactions.find((t) => t.id === selectedTransactionId) || null

  const transactionColumns = useMemo(() => getTransactionColumns(), [])

  const invoiceColumns = useMemo(() => getInvoiceColumns(), [])

  const billColumns = useMemo(() => getBillColumns(), [])

  const matchedColumns = useMemo(
    () =>
      getMatchedColumns({
        onUnmatch: unmatchItems,
      }),
    [unmatchItems]
  )

  const canMatch = selectedTransactionId && selectedMatchableId

  return (
    <div>
      <div className="flex justify-between items-center mb-4">
        <h2 className="text-2xl font-bold">Transaction Matching</h2>
        <Button onClick={matchItems} disabled={!canMatch}>
          Match Items
        </Button>
      </div>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
        {/* Left Panel: Bank Transactions */}
        <div>
          <h3 className="text-lg font-semibold mb-2">Bank Transactions</h3>
          <p className="text-sm text-muted-foreground mb-4">
            Select a transaction to begin matching.
          </p>
          <DataTable
            columns={transactionColumns}
            data={transactions}
            onRowClick={(row) => selectTransaction(row.id)}
            selectedId={selectedTransactionId}
          />
        </div>

        {/* Right Panel: Matching Items */}
        <div>
          <h3 className="text-lg font-semibold mb-2">Potential Matches</h3>
          {!selectedTransaction ? (
            <div className="h-full flex items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/30 p-8">
              <p className="text-muted-foreground">
                Select a transaction to see potential matches.
              </p>
            </div>
          ) : selectedTransaction.amount > 0 ? (
            <div>
              <h4 className="text-md font-semibold mb-2 text-muted-foreground">
                Unpaid Invoices
              </h4>
              <DataTable
                columns={invoiceColumns}
                data={invoices}
                onRowClick={(row) => selectMatchable(row.id)}
                selectedId={selectedMatchableId}
              />
            </div>
          ) : (
            <div>
              <h4 className="text-md font-semibold mb-2 text-muted-foreground">
                Unpaid Bills
              </h4>
              <DataTable
                columns={billColumns}
                data={bills}
                onRowClick={(row) => {
                  console.log(\`[UI] Selected matchable bill ID\`)
                  selectMatchable(row.id)
                }}
                selectedId={selectedMatchableId}
              />
            </div>
          )}
        </div>
      </div>

      {matchedItems.length > 0 && (
        <div className="mt-12">
          <h3 className="text-lg font-semibold mb-2">Matched Transactions</h3>
          <DataTable columns={matchedColumns} data={matchedItems} />
        </div>
      )}
    </div>
  )
}
`;
    await runTest(sourceCode, patchCode, expectedResult);
  });
});