import { describe, it, expect, beforeEach, afterEach } from "bun:test"
import { mkdirSync, writeFileSync, rmSync, chmodSync } from "fs"
import { join } from "path"
import { tmpdir } from "os"
import type { LoadedSkill } from "./types"

const TEST_DIR = join(tmpdir(), "async-loader-test-" + Date.now())
const SKILLS_DIR = join(TEST_DIR, ".opencode", "skill")

function createTestSkill(name: string, content: string, mcpJson?: object): string {
  const skillDir = join(SKILLS_DIR, name)
  mkdirSync(skillDir, { recursive: true })
  const skillPath = join(skillDir, "SKILL.md")
  writeFileSync(skillPath, content)
  if (mcpJson) {
    writeFileSync(join(skillDir, "mcp.json"), JSON.stringify(mcpJson, null, 2))
  }
  return skillDir
}

function createDirectSkill(name: string, content: string): string {
  mkdirSync(SKILLS_DIR, { recursive: true })
  const skillPath = join(SKILLS_DIR, `${name}.md`)
  writeFileSync(skillPath, content)
  return skillPath
}

describe("async-loader", () => {
  beforeEach(() => {
    mkdirSync(TEST_DIR, { recursive: true })
  })

  afterEach(() => {
    rmSync(TEST_DIR, { recursive: true, force: true })
  })

  describe("discoverSkillsInDirAsync", () => {
    it("returns empty array for non-existent directory", async () => {
      // #given - non-existent directory
      const nonExistentDir = join(TEST_DIR, "does-not-exist")

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const skills = await discoverSkillsInDirAsync(nonExistentDir)

      // #then - should return empty array, not throw
      expect(skills).toEqual([])
    })

    it("discovers skills from SKILL.md in directory", async () => {
      // #given
      const skillContent = `---
name: test-skill
description: A test skill
---
This is the skill body.
`
      createTestSkill("test-skill", skillContent)

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const skills = await discoverSkillsInDirAsync(SKILLS_DIR)

      // #then
      expect(skills).toHaveLength(1)
      expect(skills[0].name).toBe("test-skill")
      expect(skills[0].definition.description).toContain("A test skill")
    })

    it("discovers skills from {name}.md pattern in directory", async () => {
      // #given
      const skillContent = `---
name: named-skill
description: Named pattern skill
---
Skill body.
`
      const skillDir = join(SKILLS_DIR, "named-skill")
      mkdirSync(skillDir, { recursive: true })
      writeFileSync(join(skillDir, "named-skill.md"), skillContent)

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const skills = await discoverSkillsInDirAsync(SKILLS_DIR)

      // #then
      expect(skills).toHaveLength(1)
      expect(skills[0].name).toBe("named-skill")
    })

    it("discovers direct .md files", async () => {
      // #given
      const skillContent = `---
name: direct-skill
description: Direct markdown file
---
Direct skill.
`
      createDirectSkill("direct-skill", skillContent)

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const skills = await discoverSkillsInDirAsync(SKILLS_DIR)

      // #then
      expect(skills).toHaveLength(1)
      expect(skills[0].name).toBe("direct-skill")
    })

    it("skips entries starting with dot", async () => {
      // #given
      const validContent = `---
name: valid-skill
---
Valid.
`
      const hiddenContent = `---
name: hidden-skill
---
Hidden.
`
      createTestSkill("valid-skill", validContent)
      createTestSkill(".hidden-skill", hiddenContent)

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const skills = await discoverSkillsInDirAsync(SKILLS_DIR)

      // #then - only valid-skill should be discovered
      expect(skills).toHaveLength(1)
      expect(skills[0]?.name).toBe("valid-skill")
    })

    it("skips invalid files and continues with valid ones", async () => {
      // #given - one valid, one invalid (unreadable)
      const validContent = `---
name: valid-skill
---
Valid skill.
`
      const invalidContent = `---
name: invalid-skill
---
Invalid skill.
`
      createTestSkill("valid-skill", validContent)
      const invalidDir = createTestSkill("invalid-skill", invalidContent)
      const invalidFile = join(invalidDir, "SKILL.md")
      
      // Make file unreadable on Unix systems
      if (process.platform !== "win32") {
        chmodSync(invalidFile, 0o000)
      }

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const skills = await discoverSkillsInDirAsync(SKILLS_DIR)

      // #then - should skip invalid and return only valid
      expect(skills.length).toBeGreaterThanOrEqual(1)
      expect(skills.some((s: LoadedSkill) => s.name === "valid-skill")).toBe(true)

      // Cleanup: restore permissions before cleanup
      if (process.platform !== "win32") {
        chmodSync(invalidFile, 0o644)
      }
    })

    it("discovers multiple skills correctly", async () => {
      // #given
      const skill1 = `---
name: skill-one
description: First skill
---
Skill one.
`
      const skill2 = `---
name: skill-two
description: Second skill
---
Skill two.
`
      createTestSkill("skill-one", skill1)
      createTestSkill("skill-two", skill2)

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const asyncSkills = await discoverSkillsInDirAsync(SKILLS_DIR)

      // #then
      expect(asyncSkills.length).toBe(2)
      expect(asyncSkills.map((s: LoadedSkill) => s.name).sort()).toEqual(["skill-one", "skill-two"])
      
      const skill1Result = asyncSkills.find((s: LoadedSkill) => s.name === "skill-one")
      expect(skill1Result?.definition.description).toContain("First skill")
    })

    it("loads MCP config from frontmatter", async () => {
      // #given
      const skillContent = `---
name: mcp-skill
description: Skill with MCP
mcp:
  sqlite:
    command: uvx
    args: [mcp-server-sqlite]
---
MCP skill.
`
      createTestSkill("mcp-skill", skillContent)

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const skills = await discoverSkillsInDirAsync(SKILLS_DIR)

      // #then
      const skill = skills.find((s: LoadedSkill) => s.name === "mcp-skill")
      expect(skill?.mcpConfig).toBeDefined()
      expect(skill?.mcpConfig?.sqlite).toBeDefined()
      expect(skill?.mcpConfig?.sqlite?.command).toBe("uvx")
    })

    it("loads MCP config from mcp.json file", async () => {
      // #given
      const skillContent = `---
name: json-mcp-skill
description: Skill with mcp.json
---
Skill body.
`
      const mcpJson = {
        mcpServers: {
          playwright: {
            command: "npx",
            args: ["@playwright/mcp"]
          }
        }
      }
      createTestSkill("json-mcp-skill", skillContent, mcpJson)

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const skills = await discoverSkillsInDirAsync(SKILLS_DIR)

      // #then
      const skill = skills.find((s: LoadedSkill) => s.name === "json-mcp-skill")
      expect(skill?.mcpConfig?.playwright).toBeDefined()
      expect(skill?.mcpConfig?.playwright?.command).toBe("npx")
    })

    it("prioritizes mcp.json over frontmatter MCP", async () => {
      // #given
      const skillContent = `---
name: priority-test
mcp:
  from-yaml:
    command: yaml-cmd
---
Skill.
`
      const mcpJson = {
        mcpServers: {
          "from-json": {
            command: "json-cmd"
          }
        }
      }
      createTestSkill("priority-test", skillContent, mcpJson)

      // #when
      const { discoverSkillsInDirAsync } = await import("./async-loader")
      const skills = await discoverSkillsInDirAsync(SKILLS_DIR)

      // #then - mcp.json should take priority
      const skill = skills.find((s: LoadedSkill) => s.name === "priority-test")
      expect(skill?.mcpConfig?.["from-json"]).toBeDefined()
      expect(skill?.mcpConfig?.["from-yaml"]).toBeUndefined()
    })
  })

  describe("mapWithConcurrency", () => {
    it("processes items with concurrency limit", async () => {
      // #given
      const { mapWithConcurrency } = await import("./async-loader")
      const items = Array.from({ length: 50 }, (_, i) => i)
      let maxConcurrent = 0
      let currentConcurrent = 0

      const mapper = async (item: number) => {
        currentConcurrent++
        maxConcurrent = Math.max(maxConcurrent, currentConcurrent)
        await new Promise(resolve => setTimeout(resolve, 10))
        currentConcurrent--
        return item * 2
      }

      // #when
      const results = await mapWithConcurrency(items, mapper, 16)

      // #then
      expect(results).toEqual(items.map(i => i * 2))
      expect(maxConcurrent).toBeLessThanOrEqual(16)
      expect(maxConcurrent).toBeGreaterThan(1) // Should actually run concurrently
    })

    it("handles empty array", async () => {
      // #given
      const { mapWithConcurrency } = await import("./async-loader")

      // #when
      const results = await mapWithConcurrency([], async (x: number) => x * 2, 16)

      // #then
      expect(results).toEqual([])
    })

    it("handles single item", async () => {
      // #given
      const { mapWithConcurrency } = await import("./async-loader")

      // #when
      const results = await mapWithConcurrency([42], async (x: number) => x * 2, 16)

      // #then
      expect(results).toEqual([84])
    })
  })

  describe("loadSkillFromPathAsync", () => {
    it("loads skill from valid path", async () => {
      // #given
      const skillContent = `---
name: path-skill
description: Loaded from path
---
Path skill.
`
      const skillDir = createTestSkill("path-skill", skillContent)
      const skillPath = join(skillDir, "SKILL.md")

      // #when
      const { loadSkillFromPathAsync } = await import("./async-loader")
      const skill = await loadSkillFromPathAsync(skillPath, skillDir, "path-skill", "opencode-project")

      // #then
      expect(skill).not.toBeNull()
      expect(skill?.name).toBe("path-skill")
      expect(skill?.scope).toBe("opencode-project")
    })

    it("returns null for invalid path", async () => {
      // #given
      const invalidPath = join(TEST_DIR, "nonexistent.md")

      // #when
      const { loadSkillFromPathAsync } = await import("./async-loader")
      const skill = await loadSkillFromPathAsync(invalidPath, TEST_DIR, "invalid", "opencode")

      // #then
      expect(skill).toBeNull()
    })

    it("returns null for malformed skill file", async () => {
      // #given
      const malformedContent = "This is not valid frontmatter content\nNo YAML here!"
      mkdirSync(SKILLS_DIR, { recursive: true })
      const malformedPath = join(SKILLS_DIR, "malformed.md")
      writeFileSync(malformedPath, malformedContent)

      // #when
      const { loadSkillFromPathAsync } = await import("./async-loader")
      const skill = await loadSkillFromPathAsync(malformedPath, SKILLS_DIR, "malformed", "user")

      // #then
      expect(skill).not.toBeNull() // parseFrontmatter handles missing frontmatter gracefully
    })
  })

  describe("loadMcpJsonFromDirAsync", () => {
    it("loads mcp.json with mcpServers format", async () => {
      // #given
      mkdirSync(SKILLS_DIR, { recursive: true })
      const mcpJson = {
        mcpServers: {
          test: {
            command: "test-cmd",
            args: ["arg1"]
          }
        }
      }
      writeFileSync(join(SKILLS_DIR, "mcp.json"), JSON.stringify(mcpJson))

      // #when
      const { loadMcpJsonFromDirAsync } = await import("./async-loader")
      const config = await loadMcpJsonFromDirAsync(SKILLS_DIR)

      // #then
      expect(config).toBeDefined()
      expect(config?.test).toBeDefined()
      expect(config?.test?.command).toBe("test-cmd")
    })

    it("returns undefined for non-existent mcp.json", async () => {
      // #given
      mkdirSync(SKILLS_DIR, { recursive: true })

      // #when
      const { loadMcpJsonFromDirAsync } = await import("./async-loader")
      const config = await loadMcpJsonFromDirAsync(SKILLS_DIR)

      // #then
      expect(config).toBeUndefined()
    })

    it("returns undefined for invalid JSON", async () => {
      // #given
      mkdirSync(SKILLS_DIR, { recursive: true })
      writeFileSync(join(SKILLS_DIR, "mcp.json"), "{ invalid json }")

      // #when
      const { loadMcpJsonFromDirAsync } = await import("./async-loader")
      const config = await loadMcpJsonFromDirAsync(SKILLS_DIR)

      // #then
      expect(config).toBeUndefined()
    })

    it("supports direct format without mcpServers", async () => {
      // #given
      mkdirSync(SKILLS_DIR, { recursive: true })
      const mcpJson = {
        direct: {
          command: "direct-cmd",
          args: ["arg"]
        }
      }
      writeFileSync(join(SKILLS_DIR, "mcp.json"), JSON.stringify(mcpJson))

      // #when
      const { loadMcpJsonFromDirAsync } = await import("./async-loader")
      const config = await loadMcpJsonFromDirAsync(SKILLS_DIR)

      // #then
      expect(config?.direct).toBeDefined()
      expect(config?.direct?.command).toBe("direct-cmd")
    })
  })
})
