/**
 * Tests for OpenCode custom tools
 * 
 * Tests the 7 custom tools: run-tests, check-coverage, security-audit, 
 * format-code, lint-check, git-summary, changed-files
 */

const assert = require("node:assert")
const fs = require("node:fs")
const os = require("node:os")
const path = require("node:path")
const { spawnSync } = require("node:child_process")
const { pathToFileURL } = require("node:url")

function runTest(name, fn) {
  return Promise.resolve()
    .then(fn)
    .then(() => {
      console.log(`  ✓ ${name}`)
      return { passed: 1, failed: 0 }
    })
    .catch((error) => {
      console.log(`  ✗ ${name}`)
      console.error(`    ${error.stack || error.message}`)
      return { passed: 0, failed: 1 }
    })
}

async function loadTools() {
  const repoRoot = path.join(__dirname, "..")
  const buildResult = spawnSync("node", [path.join(repoRoot, "scripts", "build-opencode.js")], {
    cwd: repoRoot,
    encoding: "utf8",
  })
  assert.strictEqual(buildResult.status, 0, buildResult.stderr || buildResult.stdout)
  
  const toolsDir = path.join(repoRoot, ".opencode", "dist", "tools")
  const tools = {}
  
  // Load each tool
  const toolFiles = [
    "format-code.js",
    "lint-check.js",
    "git-summary.js",
    "changed-files.js",
    "run-tests.js",
    "check-coverage.js",
    "security-audit.js",
  ]
  
  for (const toolFile of toolFiles) {
    const toolPath = path.join(toolsDir, toolFile)
    if (fs.existsSync(toolPath)) {
      const toolUrl = pathToFileURL(toolPath).href
      const toolModule = await import(toolUrl)
      const toolName = toolFile.replace(".js", "").replace("-", "")
      tools[toolName] = toolModule.default || toolModule
    }
  }
  
  return tools
}

function createMockContext(projectDir) {
  return {
    worktree: projectDir,
    directory: projectDir,
  }
}

async function withTempProject(files, fn) {
  const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-opencode-tools-"))
  try {
    for (const file of files) {
      const filePath = path.join(projectDir, file)
      fs.mkdirSync(path.dirname(filePath), { recursive: true })
      fs.writeFileSync(filePath, "")
    }
    return await fn(projectDir)
  } finally {
    fs.rmSync(projectDir, { recursive: true, force: true })
  }
}

async function main() {
  console.log("\n=== Testing OpenCode custom tools ===\n")

  const tools = await loadTools()
  const tests = []

  // Test format-code tool
  if (tools.formatcode) {
    tests.push([
      "format-code: detects TypeScript formatter",
      async () => withTempProject(
        ["tsconfig.json", "src/index.ts"],
        async (projectDir) => {
          const context = createMockContext(projectDir)
          const result = await tools.formatcode.execute(
            { filePath: "src/index.ts" },
            context
          )
          const parsed = JSON.parse(result)
          assert.strictEqual(parsed.success, true)
          assert.ok(["biome", "prettier"].includes(parsed.formatter))
          assert.ok(parsed.command.includes("src/index.ts"))
        }
      ),
    ])

    tests.push([
      "format-code: normalizes Windows backslash paths to forward slashes",
      async () => withTempProject(
        ["tsconfig.json", "src/index.ts"],
        async (projectDir) => {
          const context = createMockContext(projectDir)
          const result = await tools.formatcode.execute(
            { filePath: "src\\index.ts" },
            context
          )
          const parsed = JSON.parse(result)
          assert.strictEqual(parsed.success, true)
          assert.ok(parsed.command.includes("src/index.ts"), `expected forward slashes in command: ${parsed.command}`)
          assert.ok(!parsed.command.includes("src\\index.ts"), `unexpected backslashes in command: ${parsed.command}`)
        }
      ),
    ])

    tests.push([
      "format-code: detects Python formatter",
      async () => withTempProject(
        ["pyproject.toml", "src/main.py"],
        async (projectDir) => {
          const context = createMockContext(projectDir)
          const result = await tools.formatcode.execute(
            { filePath: "src/main.py" },
            context
          )
          const parsed = JSON.parse(result)
          assert.strictEqual(parsed.success, true)
          assert.strictEqual(parsed.formatter, "black")
          assert.ok(parsed.command.includes("src/main.py"))
        }
      ),
    ])

    tests.push([
      "format-code: handles unsupported file types",
      async () => withTempProject(
        ["README.md"],
        async (projectDir) => {
          const context = createMockContext(projectDir)
          const result = await tools.formatcode.execute(
            { filePath: "README.md" },
            context
          )
          const parsed = JSON.parse(result)
          // .md files are supported by prettier, so this should succeed
          assert.strictEqual(parsed.success, true)
          assert.strictEqual(parsed.formatter, "prettier")
          assert.ok(parsed.command.includes("README.md"))
        }
      ),
    ])
  }

  // Test lint-check tool
  if (tools.lintcheck) {
    tests.push([
      "lint-check: detects ESLint",
      async () => withTempProject(
        [".eslintrc.json", "src/index.ts"],
        async (projectDir) => {
          const context = createMockContext(projectDir)
          const result = await tools.lintcheck.execute(
            { target: "src" },
            context
          )
          const parsed = JSON.parse(result)
          assert.strictEqual(parsed.success, true)
          assert.strictEqual(parsed.linter, "eslint")
          assert.ok(parsed.command.includes("src"))
        }
      ),
    ])

    tests.push([
      "lint-check: detects Biome",
      async () => withTempProject(
        ["biome.json", "src/index.ts"],
        async (projectDir) => {
          const context = createMockContext(projectDir)
          const result = await tools.lintcheck.execute(
            { target: "src" },
            context
          )
          const parsed = JSON.parse(result)
          assert.strictEqual(parsed.success, true)
          assert.strictEqual(parsed.linter, "biome")
          assert.ok(parsed.command.includes("src"))
        }
      ),
    ])
  }

  // Test git-summary tool
  if (tools.gitsummary) {
    tests.push([
      "git-summary: returns git information",
      async () => withTempProject(
        [],
        async (projectDir) => {
          // Initialize git repo
          spawnSync("git", ["init"], { cwd: projectDir })
          spawnSync("git", ["config", "user.email", "test@test.com"], { cwd: projectDir })
          spawnSync("git", ["config", "user.name", "Test"], { cwd: projectDir })
          
          // Create a file and commit
          fs.writeFileSync(path.join(projectDir, "test.txt"), "test")
          spawnSync("git", ["add", "test.txt"], { cwd: projectDir })
          spawnSync("git", ["commit", "-m", "test commit"], { cwd: projectDir })
          
          const context = createMockContext(projectDir)
          const result = await tools.gitsummary.execute(
            { depth: 1, includeDiff: false },
            context
          )
          const parsed = JSON.parse(result)
          assert.ok(parsed.branch)
          assert.ok(parsed.log)
          assert.ok(parsed.log.includes("test commit"))
        }
      ),
    ])
  }

  // Test changed-files tool
  if (tools.changedfiles) {
    tests.push([
      "changed-files: reports an actionable, scrubbed error when plugins/lib is missing",
      async () => withTempProject([], async (projectDir) => {
        const repoRoot = path.join(__dirname, "..")
        const libDir = path.join(repoRoot, ".opencode", "dist", "plugins", "lib")
        const backupDir = path.join(
          repoRoot,
          ".opencode",
          "dist",
          "plugins",
          "lib.missing-store-test-backup"
        )
        fs.renameSync(libDir, backupDir)
        try {
          const context = createMockContext(projectDir)
          await assert.rejects(
            () => tools.changedfiles.execute({}, context),
            (error) => {
              assert.ok(error instanceof Error)
              assert.ok(
                error.message.includes("ecc repair --target opencode"),
                "Expected the error to point at the repair command"
              )
              assert.ok(
                !error.message.includes(repoRoot),
                "Error message must not leak the local filesystem path"
              )
              assert.ok(
                !error.message.includes("Original error"),
                "Error message must not include the raw underlying loader error"
              )
              return true
            }
          )
        } finally {
          fs.renameSync(backupDir, libDir)
        }
      }),
    ])

    tests.push([
      "changed-files: renders tracked changes once plugins/lib is present",
      async () => withTempProject([], async (projectDir) => {
        const repoRoot = path.join(__dirname, "..")
        const storeUrl = pathToFileURL(
          path.join(repoRoot, ".opencode", "dist", "plugins", "lib", "changed-files-store.js")
        ).href
        const store = await import(storeUrl)
        store.initStore(projectDir)
        store.clearChanges()
        store.recordChange("src/example.ts", "modified")
        store.recordChange("src/new-file.ts", "added")

        try {
          const context = createMockContext(projectDir)
          const result = await tools.changedfiles.execute({ format: "json" }, context)
          const parsed = JSON.parse(result)
          assert.strictEqual(parsed.changed, true)
          assert.ok(
            parsed.files.some(
              (f) =>
                f.path === path.normalize("src/example.ts") && f.changeType === "modified"
            )
          )
          assert.ok(
            parsed.files.some(
              (f) => f.path === path.normalize("src/new-file.ts") && f.changeType === "added"
            )
          )
        } finally {
          store.clearChanges()
        }
      }),
    ])
  }

  // Run all tests
  let passed = 0
  let failed = 0
  for (const [name, fn] of tests) {
    const result = await runTest(name, fn)
    passed += result.passed
    failed += result.failed
  }

  console.log(`\nPassed: ${passed}`)
  console.log(`Failed: ${failed}`)
  process.exit(failed > 0 ? 1 : 0)
}

main()
