"""Tests for lockfile parsing."""

import json
from pathlib import Path

import pytest

from dekk.detection.lockfile import (
    LockedDependency,
    LockfileInfo,
    LockfileKind,
    LockfileParser,
)


@pytest.fixture
def parser():
    return LockfileParser()


# ---------------------------------------------------------------------------
# LockedDependency / LockfileInfo
# ---------------------------------------------------------------------------


class TestLockfileInfo:
    def test_package_count(self):
        info = LockfileInfo(
            kind=LockfileKind.CARGO,
            path=Path("Cargo.lock"),
            packages=(
                LockedDependency(name="foo", version="1.0.0"),
                LockedDependency(name="bar", version="2.0.0"),
            ),
        )
        assert info.package_count == 2

    def test_package_names(self):
        info = LockfileInfo(
            kind=LockfileKind.CARGO,
            path=Path("Cargo.lock"),
            packages=(
                LockedDependency(name="foo", version="1.0.0"),
                LockedDependency(name="bar", version="2.0.0"),
            ),
        )
        assert info.package_names == ("foo", "bar")

    def test_get_package_found(self):
        dep = LockedDependency(name="foo", version="1.0.0")
        info = LockfileInfo(
            kind=LockfileKind.CARGO,
            path=Path("Cargo.lock"),
            packages=(dep,),
        )
        assert info.get_package("foo") == dep

    def test_get_package_not_found(self):
        info = LockfileInfo(
            kind=LockfileKind.CARGO,
            path=Path("Cargo.lock"),
            packages=(),
        )
        assert info.get_package("nonexistent") is None

    def test_dependency_graph(self):
        info = LockfileInfo(
            kind=LockfileKind.CARGO,
            path=Path("Cargo.lock"),
            packages=(
                LockedDependency(name="app", version="1.0.0", dependencies=("lib",)),
                LockedDependency(name="lib", version="0.5.0", dependencies=("external",)),
            ),
        )
        graph = info.dependency_graph()
        assert graph["app"] == ("lib",)
        # "external" is not in packages, so filtered out
        assert graph["lib"] == ()

    def test_find_outdated(self):
        info = LockfileInfo(
            kind=LockfileKind.CARGO,
            path=Path("Cargo.lock"),
            packages=(
                LockedDependency(name="foo", version="1.0.0"),
                LockedDependency(name="bar", version="2.0.0"),
                LockedDependency(name="baz", version="3.0.0"),
            ),
        )
        latest = {"foo": "1.1.0", "bar": "2.0.0", "baz": "4.0.0"}
        outdated = info.find_outdated(latest)
        assert len(outdated) == 2
        assert ("foo", "1.0.0", "1.1.0") in outdated
        assert ("baz", "3.0.0", "4.0.0") in outdated


# ---------------------------------------------------------------------------
# Cargo.lock
# ---------------------------------------------------------------------------


class TestCargoLock:
    def test_parse_cargo_lock(self, tmp_path, parser):
        lock_content = """\
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "serde"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc123"

[[package]]
name = "tokio"
version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "mio 0.8.10",
 "pin-project-lite 0.2.13",
]

[[package]]
name = "my-app"
version = "0.1.0"
dependencies = [
 "serde 1.0.195",
 "tokio 1.35.1",
]
"""
        lock_file = tmp_path / "Cargo.lock"
        lock_file.write_text(lock_content)

        info = parser.parse(lock_file)
        assert info is not None
        assert info.kind == LockfileKind.CARGO
        assert info.package_count == 3

        serde = info.get_package("serde")
        assert serde is not None
        assert serde.version == "1.0.195"
        assert serde.checksum == "abc123"

        tokio = info.get_package("tokio")
        assert tokio is not None
        assert "mio" in tokio.dependencies
        assert "pin-project-lite" in tokio.dependencies

    def test_parse_nonexistent(self, tmp_path, parser):
        assert parser.parse(tmp_path / "Cargo.lock") is None


# ---------------------------------------------------------------------------
# package-lock.json (npm)
# ---------------------------------------------------------------------------


class TestNpmLock:
    def test_parse_npm_v3(self, tmp_path, parser):
        lock_data = {
            "name": "my-app",
            "version": "1.0.0",
            "lockfileVersion": 3,
            "packages": {
                "": {"name": "my-app", "version": "1.0.0"},
                "node_modules/express": {
                    "version": "4.18.2",
                    "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
                    "integrity": "sha512-abc123",
                    "dependencies": {"body-parser": "1.20.2"},
                },
                "node_modules/body-parser": {
                    "version": "1.20.2",
                    "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
                },
            },
        }
        lock_file = tmp_path / "package-lock.json"
        lock_file.write_text(json.dumps(lock_data))

        info = parser.parse(lock_file)
        assert info is not None
        assert info.kind == LockfileKind.NPM
        assert info.lockfile_version == "3"
        assert info.package_count == 2  # excludes root ""

        express = info.get_package("express")
        assert express is not None
        assert express.version == "4.18.2"
        assert "body-parser" in express.dependencies

    def test_parse_npm_v1(self, tmp_path, parser):
        lock_data = {
            "name": "old-app",
            "lockfileVersion": 1,
            "dependencies": {
                "lodash": {
                    "version": "4.17.21",
                    "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
                    "integrity": "sha512-xyz",
                },
            },
        }
        lock_file = tmp_path / "package-lock.json"
        lock_file.write_text(json.dumps(lock_data))

        info = parser.parse(lock_file)
        assert info is not None
        assert info.lockfile_version == "1"
        lodash = info.get_package("lodash")
        assert lodash is not None
        assert lodash.version == "4.17.21"


# ---------------------------------------------------------------------------
# yarn.lock
# ---------------------------------------------------------------------------


class TestYarnLock:
    def test_parse_yarn_v1(self, tmp_path, parser):
        lock_content = """\
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


express@^4.18.0:
  version "4.18.2"
  resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#abc"
  integrity sha512-something
  dependencies:
    body-parser "1.20.2"
    cookie "0.5.0"

body-parser@1.20.2:
  version "1.20.2"
  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz"
"""
        lock_file = tmp_path / "yarn.lock"
        lock_file.write_text(lock_content)

        info = parser.parse(lock_file)
        assert info is not None
        assert info.kind == LockfileKind.YARN
        assert info.package_count == 2

        express = info.get_package("express")
        assert express is not None
        assert express.version == "4.18.2"
        assert "body-parser" in express.dependencies
        assert "cookie" in express.dependencies

    def test_parse_scoped_package(self, tmp_path, parser):
        lock_content = """\
"@types/node@^20.0.0":
  version "20.10.0"
  resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.0.tgz"
"""
        lock_file = tmp_path / "yarn.lock"
        lock_file.write_text(lock_content)

        info = parser.parse(lock_file)
        assert info is not None
        pkg = info.get_package("@types/node")
        assert pkg is not None
        assert pkg.version == "20.10.0"


# ---------------------------------------------------------------------------
# poetry.lock
# ---------------------------------------------------------------------------


class TestPoetryLock:
    def test_parse_poetry_lock(self, tmp_path, parser):
        lock_content = """\
# This file is automatically @generated by Poetry and should not be changed.

[[package]]
name = "requests"
version = "2.31.0"

[package.dependencies]
certifi = ">=2017.4.17"
urllib3 = ">=1.21.1,<3"

[[package]]
name = "certifi"
version = "2023.11.17"

[[package]]
name = "urllib3"
version = "2.1.0"
"""
        lock_file = tmp_path / "poetry.lock"
        lock_file.write_text(lock_content)

        info = parser.parse(lock_file)
        assert info is not None
        assert info.kind == LockfileKind.POETRY
        assert info.package_count == 3

        requests = info.get_package("requests")
        assert requests is not None
        assert requests.version == "2.31.0"
        assert "certifi" in requests.dependencies
        assert "urllib3" in requests.dependencies


# ---------------------------------------------------------------------------
# pnpm-lock.yaml
# ---------------------------------------------------------------------------


class TestPnpmLock:
    def test_parse_pnpm_v9(self, tmp_path, parser):
        lock_content = """\
lockfileVersion: '9.0'

settings:
  autoInstallPeers: true

packages:

  express@4.18.2:
    resolution: {integrity: sha512-abc}

  body-parser@1.20.2:
    resolution: {integrity: sha512-xyz}
"""
        lock_file = tmp_path / "pnpm-lock.yaml"
        lock_file.write_text(lock_content)

        info = parser.parse(lock_file)
        assert info is not None
        assert info.kind == LockfileKind.PNPM
        assert info.lockfile_version == "9.0"
        assert info.package_count == 2

        express = info.get_package("express")
        assert express is not None
        assert express.version == "4.18.2"

    def test_parse_pnpm_v6(self, tmp_path, parser):
        lock_content = """\
lockfileVersion: '6.0'

packages:

  /express@4.18.2:
    resolution: {integrity: sha512-abc}

  /@types/node@20.10.0:
    resolution: {integrity: sha512-xyz}
"""
        lock_file = tmp_path / "pnpm-lock.yaml"
        lock_file.write_text(lock_content)

        info = parser.parse(lock_file)
        assert info is not None
        assert info.package_count == 2

        types_node = info.get_package("@types/node")
        assert types_node is not None
        assert types_node.version == "20.10.0"


# ---------------------------------------------------------------------------
# Gemfile.lock
# ---------------------------------------------------------------------------


class TestGemfileLock:
    def test_parse_gemfile_lock(self, tmp_path, parser):
        lock_content = """\
GEM
  remote: https://rubygems.org/
  specs:
    rails (7.1.0)
      actioncable (= 7.1.0)
      actionmailer (= 7.1.0)
    actioncable (7.1.0)
    actionmailer (7.1.0)

PLATFORMS
  ruby

DEPENDENCIES
  rails (~> 7.1)

BUNDLED WITH
   2.4.22
"""
        lock_file = tmp_path / "Gemfile.lock"
        lock_file.write_text(lock_content)

        info = parser.parse(lock_file)
        assert info is not None
        assert info.kind == LockfileKind.GEMFILE
        assert info.package_count == 3

        rails = info.get_package("rails")
        assert rails is not None
        assert rails.version == "7.1.0"


# ---------------------------------------------------------------------------
# detect_and_parse
# ---------------------------------------------------------------------------


class TestDetectAndParse:
    def test_finds_multiple_lockfiles(self, tmp_path, parser):
        # Create Cargo.lock
        cargo_lock = tmp_path / "Cargo.lock"
        cargo_lock.write_text('version = 4\n\n[[package]]\nname = "foo"\nversion = "1.0.0"\n')

        # Create package-lock.json
        npm_lock = tmp_path / "package-lock.json"
        npm_lock.write_text(
            json.dumps(
                {
                    "lockfileVersion": 3,
                    "packages": {
                        "": {"name": "test"},
                        "node_modules/bar": {"version": "2.0.0"},
                    },
                }
            )
        )

        results = parser.detect_and_parse(tmp_path)
        assert len(results) == 2
        kinds = {r.kind for r in results}
        assert LockfileKind.CARGO in kinds
        assert LockfileKind.NPM in kinds

    def test_empty_directory(self, tmp_path, parser):
        assert parser.detect_and_parse(tmp_path) == []

    def test_nonexistent_directory(self, tmp_path, parser):
        assert parser.detect_and_parse(tmp_path / "no_such_dir") == []

    def test_unknown_filename(self, tmp_path, parser):
        (tmp_path / "unknown.lock").write_text("data")
        assert parser.parse(tmp_path / "unknown.lock") is None
