/**
 * Relative Imports Tests
 *
 * E2E tests for the `generate-metadata` command with relative imports:
 * - Lock files correctly include transitive dependencies
 * - Staleness propagates through import chains
 * - Various import patterns handled correctly
 */

import { expect, test } from "bun:test";
import { writeFile, readFile, mkdir } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";

// TODO: re-enable Python tests on CI if python feature is included by default
const isCI = process.env["CI_MINIMAL_FEATURES"] === "true";

const defaultMetadata = `summary: "Test"
schema:
  type: object
  properties: {}
lock: ""
`;

// =============================================================================
// Test 1: TS basic import with npm dependency propagation
// =============================================================================

test("TS: imported script's npm dep appears in importer's lock", { timeout: 60000 }, async () => {
  await withTestBackend(async (backend, tempDir) => {
    await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes: ["**"]
excludes: []`);

    await mkdir(`${tempDir}/f/test`, { recursive: true });

    const scriptA = `import { helper } from "./script_b.ts";
export async function main() { return helper(); }
`;
    const scriptB = `import _ from "lodash";
export function helper() { return _.VERSION; }
`;

    await writeFile(`${tempDir}/f/test/script_a.ts`, scriptA);
    await writeFile(`${tempDir}/f/test/script_a.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/script_b.ts`, scriptB);
    await writeFile(`${tempDir}/f/test/script_b.script.yaml`, defaultMetadata);

    const result = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes"],
      tempDir
    );
    expect(result.code, `generate-metadata failed:\nSTDOUT: ${result.stdout}\nSTDERR: ${result.stderr}`).toBe(0);

    const lockA = await readFile(`${tempDir}/f/test/script_a.script.lock`, "utf-8").catch(() => "");
    const lockB = await readFile(`${tempDir}/f/test/script_b.script.lock`, "utf-8").catch(() => "");

    expect(lockB).toContain("lodash");
    expect(lockA).toContain("lodash");
  });
});

// =============================================================================
// Test 2: TS chained imports - dependency propagates through chain
// =============================================================================

test("TS: chained imports propagate npm deps through entire chain", { timeout: 60000 }, async () => {
  await withTestBackend(async (backend, tempDir) => {
    await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes: ["**"]
excludes: []`);

    await mkdir(`${tempDir}/f/test`, { recursive: true });

    const scriptA = `import { utilB } from "./script_b.ts";
export async function main() { return utilB(); }
`;
    const scriptB = `import { utilC } from "./script_c.ts";
export function utilB() { return utilC() + " B"; }
`;
    const scriptC = `import _ from "lodash";
export function utilC() { return _.VERSION; }
`;

    await writeFile(`${tempDir}/f/test/script_a.ts`, scriptA);
    await writeFile(`${tempDir}/f/test/script_a.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/script_b.ts`, scriptB);
    await writeFile(`${tempDir}/f/test/script_b.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/script_c.ts`, scriptC);
    await writeFile(`${tempDir}/f/test/script_c.script.yaml`, defaultMetadata);

    const result = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes"],
      tempDir
    );
    expect(result.code).toBe(0);

    const lockA = await readFile(`${tempDir}/f/test/script_a.script.lock`, "utf-8").catch(() => "");
    const lockB = await readFile(`${tempDir}/f/test/script_b.script.lock`, "utf-8").catch(() => "");
    const lockC = await readFile(`${tempDir}/f/test/script_c.script.lock`, "utf-8").catch(() => "");

    expect(lockC).toContain("lodash");
    expect(lockB).toContain("lodash");
    expect(lockA).toContain("lodash");
  });
});

// =============================================================================
// Test 3: TS circular imports - completes without hanging, locks generated
// =============================================================================

test("TS: circular imports handled gracefully with correct locks", { timeout: 60000 }, async () => {
  await withTestBackend(async (backend, tempDir) => {
    await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes: ["**"]
excludes: []`);

    await mkdir(`${tempDir}/f/test`, { recursive: true });

    // Circular: A imports B, B imports A, B has npm dep
    const scriptA = `import { funcB } from "./script_b.ts";
export function funcA() { return "A"; }
export async function main() { return funcA() + funcB(); }
`;
    const scriptB = `import { funcA } from "./script_a.ts";
import _ from "lodash";
export function funcB() { return _.VERSION + funcA(); }
`;

    await writeFile(`${tempDir}/f/test/script_a.ts`, scriptA);
    await writeFile(`${tempDir}/f/test/script_a.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/script_b.ts`, scriptB);
    await writeFile(`${tempDir}/f/test/script_b.script.yaml`, defaultMetadata);

    const result = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes"],
      tempDir
    );
    expect(result.code).toBe(0);

    const lockA = await readFile(`${tempDir}/f/test/script_a.script.lock`, "utf-8").catch(() => "");
    const lockB = await readFile(`${tempDir}/f/test/script_b.script.lock`, "utf-8").catch(() => "");

    expect(lockB).toContain("lodash");
    expect(lockA).toContain("lodash");
  });
});

// =============================================================================
// Test 4: Python basic import with pip dependency propagation
// =============================================================================

test.skipIf(isCI)("Python: imported script's pip dep appears in importer's lock", { timeout: 60000 }, async () => {
  await withTestBackend(async (backend, tempDir) => {
    await writeFile(`${tempDir}/wmill.yaml`, `includes: ["**"]
excludes: []`);

    await mkdir(`${tempDir}/f/test`, { recursive: true });

    const mainPy = `from f.test.helper import helper_func

def main():
    return helper_func()
`;
    const helperPy = `import requests

def helper_func():
    return requests.__version__
`;

    await writeFile(`${tempDir}/f/test/main.py`, mainPy);
    await writeFile(`${tempDir}/f/test/main.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/helper.py`, helperPy);
    await writeFile(`${tempDir}/f/test/helper.script.yaml`, defaultMetadata);

    const result = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes"],
      tempDir
    );
    if (result.code !== 0) {
      console.log("STDOUT:", result.stdout);
      console.log("STDERR:", result.stderr);
    }
    expect(result.code).toBe(0);

    const lockMain = await readFile(`${tempDir}/f/test/main.script.lock`, "utf-8").catch(() => "");
    const lockHelper = await readFile(`${tempDir}/f/test/helper.script.lock`, "utf-8").catch(() => "");

    expect(lockHelper).toContain("requests");
    expect(lockMain).toContain("requests");
  });
});

// =============================================================================
// Test 5: Diamond dependency - A imports B and C, both import D
// =============================================================================

test.skipIf(isCI)("Python: diamond dependency pattern propagates correctly", { timeout: 60000 }, async () => {
  await withTestBackend(async (backend, tempDir) => {
    await writeFile(`${tempDir}/wmill.yaml`, `includes: ["**"]
excludes: []`);

    await mkdir(`${tempDir}/f/test`, { recursive: true });

    // Diamond: A -> B, A -> C, B -> D, C -> D
    const scriptA = `from f.test.script_b import func_b
from f.test.script_c import func_c

def main():
    return func_b() + func_c()
`;
    const scriptB = `from f.test.script_d import func_d

def func_b():
    return "B" + func_d()
`;
    const scriptC = `from f.test.script_d import func_d

def func_c():
    return "C" + func_d()
`;
    const scriptD = `import requests

def func_d():
    return requests.__version__
`;

    await writeFile(`${tempDir}/f/test/script_a.py`, scriptA);
    await writeFile(`${tempDir}/f/test/script_a.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/script_b.py`, scriptB);
    await writeFile(`${tempDir}/f/test/script_b.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/script_c.py`, scriptC);
    await writeFile(`${tempDir}/f/test/script_c.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/script_d.py`, scriptD);
    await writeFile(`${tempDir}/f/test/script_d.script.yaml`, defaultMetadata);

    const result = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes"],
      tempDir
    );
    expect(result.code).toBe(0);

    const lockA = await readFile(`${tempDir}/f/test/script_a.script.lock`, "utf-8").catch(() => "");
    const lockB = await readFile(`${tempDir}/f/test/script_b.script.lock`, "utf-8").catch(() => "");
    const lockC = await readFile(`${tempDir}/f/test/script_c.script.lock`, "utf-8").catch(() => "");
    const lockD = await readFile(`${tempDir}/f/test/script_d.script.lock`, "utf-8").catch(() => "");

    expect(lockD).toContain("requests");
    expect(lockB).toContain("requests");
    expect(lockC).toContain("requests");
    expect(lockA).toContain("requests");
  });
});

// =============================================================================
// Test 6: Script isolation - unrelated script not marked stale
// =============================================================================

test("Script isolation: unrelated script not affected by changes", { timeout: 120000 }, async () => {
  await withTestBackend(async (backend, tempDir) => {
    await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes: ["**"]
excludes: []`);

    await mkdir(`${tempDir}/f/test`, { recursive: true });

    // A imports B, C is isolated
    const scriptA = `import { helper } from "./script_b.ts";
export async function main() { return helper(); }
`;
    const scriptB = `export function helper() { return "B"; }
`;
    const scriptC = `export async function main() { return "isolated"; }
`;

    await writeFile(`${tempDir}/f/test/script_a.ts`, scriptA);
    await writeFile(`${tempDir}/f/test/script_a.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/script_b.ts`, scriptB);
    await writeFile(`${tempDir}/f/test/script_b.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/script_c.ts`, scriptC);
    await writeFile(`${tempDir}/f/test/script_c.script.yaml`, defaultMetadata);

    // Generate initial metadata
    const initial = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes"],
      tempDir
    );
    expect(initial.code).toBe(0);

    // Verify all up to date
    const check1 = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes", "--dry-run"],
      tempDir
    );
    expect(check1.stdout).toContain("All metadata up-to-date");

    // Change script_b
    await writeFile(`${tempDir}/f/test/script_b.ts`,
      `export function helper() { return "B changed"; }
`);

    // script_a and script_b should be stale, script_c should NOT be mentioned
    const check2 = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes", "--dry-run"],
      tempDir
    );
    expect(check2.code).toBe(0);
    expect(check2.stdout).toContain("script_b");
    expect(check2.stdout).toContain("script_a");
    expect(check2.stdout).not.toMatch(/script_c/);
  });
});

// =============================================================================
// Test 7: Python relative imports with dot syntax
// =============================================================================

test.skipIf(isCI)("Python: relative imports with dot syntax work correctly", { timeout: 60000 }, async () => {
  await withTestBackend(async (backend, tempDir) => {
    await writeFile(`${tempDir}/wmill.yaml`, `includes: ["**"]
excludes: []`);

    await mkdir(`${tempDir}/f/mymodule`, { recursive: true });

    // Using relative import syntax
    const mainPy = `from .helper import helper_func

def main():
    return helper_func()
`;
    const helperPy = `import requests

def helper_func():
    return requests.__version__
`;

    await writeFile(`${tempDir}/f/mymodule/main.py`, mainPy);
    await writeFile(`${tempDir}/f/mymodule/main.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/mymodule/helper.py`, helperPy);
    await writeFile(`${tempDir}/f/mymodule/helper.script.yaml`, defaultMetadata);

    const result = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/mymodule/*", "--yes"],
      tempDir
    );
    expect(result.code).toBe(0);

    const lockMain = await readFile(`${tempDir}/f/mymodule/main.script.lock`, "utf-8").catch(() => "");
    const lockHelper = await readFile(`${tempDir}/f/mymodule/helper.script.lock`, "utf-8").catch(() => "");

    expect(lockHelper).toContain("requests");
    expect(lockMain).toContain("requests");
  });
});

// =============================================================================
// Test 8: Adding new import updates importer's lock
// =============================================================================

test.skipIf(isCI)("Python: adding new import updates importer's lock correctly", { timeout: 120000 }, async () => {
  await withTestBackend(async (backend, tempDir) => {
    await writeFile(`${tempDir}/wmill.yaml`, `includes: ["**"]
excludes: []`);

    await mkdir(`${tempDir}/f/test`, { recursive: true });

    // Initial: main imports helper, helper has no external deps
    const mainPy = `from f.test.helper import helper_func

def main():
    return helper_func()
`;
    const helperPyInitial = `def helper_func():
    return "no deps"
`;

    await writeFile(`${tempDir}/f/test/main.py`, mainPy);
    await writeFile(`${tempDir}/f/test/main.script.yaml`, defaultMetadata);
    await writeFile(`${tempDir}/f/test/helper.py`, helperPyInitial);
    await writeFile(`${tempDir}/f/test/helper.script.yaml`, defaultMetadata);

    // Generate initial locks
    const initial = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes"],
      tempDir
    );
    expect(initial.code).toBe(0);

    // Add new script with pip dep
    const utilsPy = `import requests

def get_version():
    return requests.__version__
`;
    await writeFile(`${tempDir}/f/test/utils.py`, utilsPy);
    await writeFile(`${tempDir}/f/test/utils.script.yaml`, defaultMetadata);

    // Modify helper to import utils
    const helperPyWithImport = `from f.test.utils import get_version

def helper_func():
    return get_version()
`;
    await writeFile(`${tempDir}/f/test/helper.py`, helperPyWithImport);

    // Regenerate - main should now have requests
    const afterAdd = await backend.runCLICommand(
      ["generate-metadata", "-i", "f/test/*", "--yes"],
      tempDir
    );
    expect(afterAdd.code).toBe(0);

    const lockUtils = await readFile(`${tempDir}/f/test/utils.script.lock`, "utf-8").catch(() => "");
    const lockHelper = await readFile(`${tempDir}/f/test/helper.script.lock`, "utf-8").catch(() => "");
    const lockMain = await readFile(`${tempDir}/f/test/main.script.lock`, "utf-8").catch(() => "");

    expect(lockUtils).toContain("requests");
    expect(lockHelper).toContain("requests");
    expect(lockMain).toContain("requests");
  });
});
