#!/usr/bin/env bats
# shellcheck disable=SC2016,SC2030,SC2031,SC2317

setup() {
  load helpers/setup.bash
  REAL_SHELLOPS_DIR="$SHELLOPS_DIR"
  source "$SHELLOPS_DIR/cli/install"

  resolver="$(jq_obj \
    '.registry' '{}' \
    '.order'    '[]' \
    '.channels' '{}' \
    '.tools'    '[]'
  )"
  __tools_api__load_channels

  FAKE_SHELLOPS="$(mktemp -d)"
  FAKE_PROJ="$(mktemp -d)"
}

teardown() {
  rm -rf "$FAKE_SHELLOPS" "$FAKE_PROJ"
}

reset_state() {
  export SHELLOPS_DIR="$FAKE_SHELLOPS"
  export PROJ="$FAKE_PROJ"
  __tools_api__lockfile="$FAKE_PROJ/ops.lock"
  __tools_api__scripts_dir="$FAKE_PROJ/plugins/tools"
  resolver=$(jq_set "$resolver" \
    '.registry' '{}' \
    '.order'    '[]' \
    '.tools'    '[]')
}

write_lockfile() {
  local dir="$1"
  cat > "$dir/ops.lock" <<< "$2"
}

# ==========================================================================
# __tools_api__parse_manifest: lockfile layering
# ==========================================================================

@test "parse_manifest: reads consumer lockfile" {
  reset_state
  export SHELLOPS_DIR="$FAKE_PROJ"

  write_lockfile "$FAKE_PROJ" '{
    "cargo-install": {
      "mytool": { "version": "1.0.0" }
    }
  }'

  __tools_api__parse_manifest

  local channel version
  channel=$(jq_get "$resolver" '.registry."mytool".channel')
  version=$(jq_get "$resolver" '.registry."mytool".version')
  assert_equal "$channel" "cargo-install"
  assert_equal "$version" "1.0.0"
}

@test "parse_manifest: falls back to shellops lockfile when consumer has none" {
  reset_state

  write_lockfile "$FAKE_SHELLOPS" '{
    "cargo-install": {
      "shared-tool": { "version": "2.0.0" }
    }
  }'

  __tools_api__parse_manifest

  local channel version
  channel=$(jq_get "$resolver" '.registry."shared-tool".channel')
  version=$(jq_get "$resolver" '.registry."shared-tool".version')
  assert_equal "$channel" "cargo-install"
  assert_equal "$version" "2.0.0"
}

@test "parse_manifest: merges both lockfiles without overlap" {
  reset_state

  write_lockfile "$FAKE_SHELLOPS" '{
    "cargo-install": {
      "shared-tool": { "version": "1.0.0" }
    }
  }'
  write_lockfile "$FAKE_PROJ" '{
    "go-install": {
      "proj-tool": { "version": "3.0.0", "pkg": "example.com/proj-tool" }
    }
  }'

  __tools_api__parse_manifest

  assert_equal "$(jq_get "$resolver" '.registry."shared-tool".channel')" "cargo-install"
  assert_equal "$(jq_get "$resolver" '.registry."proj-tool".channel')" "go-install"
}

@test "parse_manifest: consumer lockfile overrides shellops entry" {
  reset_state

  write_lockfile "$FAKE_SHELLOPS" '{
    "cargo-install": {
      "mytool": { "version": "1.0.0" }
    }
  }'
  write_lockfile "$FAKE_PROJ" '{
    "cargo-install": {
      "mytool": { "version": "2.5.0" }
    }
  }'

  __tools_api__parse_manifest

  assert_equal "$(jq_get "$resolver" '.registry."mytool".version')" "2.5.0"
}

@test "parse_manifest: consumer can override channel for the same tool" {
  reset_state

  write_lockfile "$FAKE_SHELLOPS" '{
    "local": {
      "mytool": { "version": "1.0.0" }
    }
  }'
  write_lockfile "$FAKE_PROJ" '{
    "cargo-install": {
      "mytool": { "version": "1.0.0" }
    }
  }'

  __tools_api__parse_manifest

  assert_equal "$(jq_get "$resolver" '.registry."mytool".channel')" "cargo-install"
}

@test "parse_manifest: errors when no lockfile exists anywhere" {
  reset_state

  run __tools_api__parse_manifest
  assert_failure
  assert_output --partial "No ops.lock found"
}

@test "parse_manifest: self-mode only reads one file" {
  reset_state
  export SHELLOPS_DIR="$FAKE_PROJ"

  write_lockfile "$FAKE_PROJ" '{
    "cargo-install": {
      "self-tool": { "version": "1.0.0" }
    }
  }'

  __tools_api__parse_manifest

  local count
  count=$(jq_get "$resolver" '.registry|length')
  assert_equal "$count" "1"
  assert_equal "$(jq_get "$resolver" '.registry."self-tool".version')" "1.0.0"
}

@test "parse_manifest: empty shellops lockfile is tolerated" {
  reset_state

  write_lockfile "$FAKE_SHELLOPS" '{}'
  write_lockfile "$FAKE_PROJ" '{
    "cargo-install": {
      "proj-tool": { "version": "1.0.0" }
    }
  }'

  __tools_api__parse_manifest

  assert_equal "$(jq_get "$resolver" '.registry."proj-tool".version')" "1.0.0"
}

@test "parse_manifest: multiple tools across channels merge correctly" {
  reset_state

  write_lockfile "$FAKE_SHELLOPS" '{
    "local": {
      "linter": { "version": "0.9.0" }
    },
    "cargo-install": {
      "formatter": { "version": "1.0.0" }
    }
  }'
  write_lockfile "$FAKE_PROJ" '{
    "cargo-install": {
      "absorber": { "version": "0.9.0" }
    }
  }'

  __tools_api__parse_manifest

  assert_equal "$(jq_get "$resolver" '.registry."linter".channel')" "local"
  assert_equal "$(jq_get "$resolver" '.registry."formatter".channel')" "cargo-install"
  assert_equal "$(jq_get "$resolver" '.registry."absorber".channel')" "cargo-install"

  local count
  count=$(jq_get "$resolver" '.registry|length')
  assert_equal "$count" "3"
}

# ==========================================================================
# channel dependency injection
# ==========================================================================

@test "inject_channel_dependencies: cargo-install injects rustup dependency" {
  reset_state

  write_lockfile "$FAKE_SHELLOPS" '{
    "system": {
      "build-essential": { "version": "" }
    }
  }'
  write_lockfile "$FAKE_PROJ" '{
    "cargo-install": {
      "mytool": { "version": "1.0.0" }
    }
  }'

  __tools_api__parse_manifest
  __tools_api__inject_channel_dependencies

  assert_equal "$(jq_get "$resolver" '.registry."rustup".channel')" "script"
  assert_equal "$(jq_get "$resolver" '.registry."rustup".version')" "1.28.2"
  assert_equal "$(jq -c '.registry."rustup".needs' <<< "$resolver")" '["build-essential"]'
  assert_equal "$(jq -c '.registry."mytool".needs' <<< "$resolver")" '["rustup"]'
}

@test "inject_channel_dependencies: cargo-binstall injects toolchain chain in order" {
  reset_state

  write_lockfile "$FAKE_SHELLOPS" '{
    "system": {
      "build-essential": { "version": "" }
    }
  }'
  write_lockfile "$FAKE_PROJ" '{
    "cargo-binstall": {
      "eza": { "version": "0.23.4" }
    }
  }'

  __tools_api__parse_manifest
  __tools_api__inject_channel_dependencies

  local order
  order=$(__tools_api__resolve_order "$(jq_get "$resolver" '.registry')")
  resolver=$(jq_set "$resolver" '.order' "$order")

  assert_equal "$(jq_get "$resolver" '.registry."cargo-binstall".channel')" "cargo-install"
  assert_equal "$(jq_get "$resolver" '.registry."cargo-binstall".version')" "1.18.0"
  assert_equal "$(jq -c '.registry."cargo-binstall".needs' <<< "$resolver")" '["rustup"]'
  assert_equal "$(jq -c '.registry."eza".needs' <<< "$resolver")" '["cargo-binstall"]'
  assert_equal "$(jq_get "$resolver" '.order[0]')" "build-essential"
  assert_equal "$(jq_get "$resolver" '.order[1]')" "rustup"
  assert_equal "$(jq_get "$resolver" '.order[2]')" "cargo-binstall"
  assert_equal "$(jq_get "$resolver" '.order[3]')" "eza"
}

@test "inject_channel_dependencies: go-install injects go dependency" {
  reset_state

  write_lockfile "$FAKE_PROJ" '{
    "go-install": {
      "lefthook": { "version": "1.13.6", "pkg": "github.com/evilmartians/lefthook" }
    }
  }'

  __tools_api__parse_manifest
  __tools_api__inject_channel_dependencies

  assert_equal "$(jq_get "$resolver" '.registry."go".channel')" "script"
  assert_equal "$(jq_get "$resolver" '.registry."go".version')" "1.25.5"
  assert_equal "$(jq -c '.registry."lefthook".needs' <<< "$resolver")" '["go"]'
}

@test "inject_channel_dependencies: explicit channel dependencies are not overwritten" {
  reset_state

  write_lockfile "$FAKE_SHELLOPS" '{
    "system": {
      "build-essential": { "version": "" }
    }
  }'
  write_lockfile "$FAKE_PROJ" '{
    "cargo-install": {
      "cargo-binstall": { "version": "9.9.9" }
    },
    "cargo-binstall": {
      "eza": { "version": "0.23.4" }
    }
  }'

  __tools_api__parse_manifest
  __tools_api__inject_channel_dependencies

  assert_equal "$(jq_get "$resolver" '.registry."cargo-binstall".version')" "9.9.9"
  assert_equal "$(jq -c '.registry."cargo-binstall".needs' <<< "$resolver")" '["rustup"]'
}

# ==========================================================================
# register_channel: plugin registration
# ==========================================================================

@test "register_channel: registers install and uninstall functions" {
  reset_state

  function test__install() { :; }
  function test__uninstall() { :; }

  register_channel \
    --name "test-channel" \
    --installer test__install \
    --uninstaller test__uninstall

  assert_equal "$(jq_get "$resolver" '.channels["test-channel"].install_fn' '')" "test__install"
  assert_equal "$(jq_get "$resolver" '.channels["test-channel"].uninstall_fn' '')" "test__uninstall"
}

@test "register_channel: registers channel dependency metadata" {
  reset_state

  function test__install() { :; }
  function test__uninstall() { :; }

  register_channel \
    --name "test-channel" \
    --installer test__install \
    --uninstaller test__uninstall \
    --dependency "test-runtime" \
    --dependency-default '{"channel":"script","version":"1.0.0"}'

  assert_equal "$(jq_get "$resolver" '.channels["test-channel"].dependency' '')" "test-runtime"
  assert_equal "$(jq -c '.channels["test-channel"].dependency_default' <<< "$resolver")" '{"channel":"script","version":"1.0.0"}'
}

@test "register_channel: channel without dependency skips dependency fields" {
  reset_state

  function test__install() { :; }
  function test__uninstall() { :; }

  register_channel \
    --name "simple" \
    --installer test__install \
    --uninstaller test__uninstall

  [[ -z "$(jq_get "$resolver" '.channels["simple"].dependency' '')" ]]
}

# ==========================================================================
# shell rc helpers
# ==========================================================================

@test "shell_rc: appends line when interactive and accepted" {
  reset_state
  source "$REAL_SHELLOPS_DIR/lib/shell_rc.api.sh"

  export HOME="$FAKE_PROJ/home"
  export SHELL="/bin/bash"
  mkdir -p "$HOME"

  function __shell_rc__is_interactive() { return 0; }

  shell_rc_offer_line \
    "Go to your PATH" \
    'export PATH="/usr/local/go/bin:$PATH"' <<< $'y\n'

  assert_equal "$(<"$HOME/.bashrc")" 'export PATH="/usr/local/go/bin:$PATH"'
}

@test "shell_rc: warns without editing in non-interactive shells" {
  reset_state
  source "$REAL_SHELLOPS_DIR/lib/shell_rc.api.sh"

  export HOME="$FAKE_PROJ/home"
  export SHELL="/bin/bash"
  mkdir -p "$HOME"

  function __shell_rc__is_interactive() { return 1; }

  run shell_rc_offer_line \
    "Go to your PATH" \
    'export PATH="/usr/local/go/bin:$PATH"'

  assert_success
  [[ ! -f "$HOME/.bashrc" ]]
  assert_output --partial 'export PATH="/usr/local/go/bin:$PATH"'
}

@test "shell_rc: skips when line already exists" {
  reset_state
  source "$REAL_SHELLOPS_DIR/lib/shell_rc.api.sh"

  export HOME="$FAKE_PROJ/home"
  export SHELL="/bin/bash"
  mkdir -p "$HOME"
  printf '%s\n' 'export PATH="/usr/local/go/bin:$PATH"' > "$HOME/.bashrc"

  function __shell_rc__is_interactive() { return 0; }

  shell_rc_offer_line \
    "Go to your PATH" \
    'export PATH="/usr/local/go/bin:$PATH"' <<< $'y\n'

  assert_equal "$(wc -l < "$HOME/.bashrc" | tr -d ' ')" "1"
}
