import logging
import os
import re

import pytest
from snakeoil.data_source import data_source
from snakeoil.fileutils import touch
from snakeoil.mappings import ImmutableDict

from pkgcore.ebuild import atom, repo_objs
from pkgcore.repository import errors as repo_errors


class TestMetadataXml:
    @staticmethod
    def get_metadata_xml(
        maintainers: tuple[tuple[str, ...], ...] = (),
        comments: tuple[str, ...] = (),
        local_use: dict[str, str] = {},
        longdescription: str | None = None,
        maint_type: str | None = None,
        proxied: str | None = None,
        stabilize_allarches: str | bool = False,
        straight_to_stable: str | bool = False,
        upstream: str = "",
    ):
        cs = "\n".join(comments)
        ms = us = ls = ""
        if maintainers:
            ms = []
            for x in maintainers:
                ms.append(f"<email>{x[0]}</email>")
                if len(x) > 1:
                    ms[-1] += f"\n<name>{x[1]}</name>"
                if len(x) > 2:
                    ms[-1] += f"\n<description>{x[2]}</description>"
                if len(x) > 3:
                    raise ValueError("maintainer data has too many fields")
            maint_type = f'type="{maint_type}"' if maint_type is not None else ""
            proxied = f'proxied="{proxied}"' if proxied is not None else ""
            ms = "\n".join(
                f"<maintainer {maint_type} {proxied}>{x}</maintainer>" for x in ms
            )
        if local_use:
            us = ["<use>"]
            for flag, desc in local_use.items():
                us.append(f'<flag name="{flag}">{desc}</flag>')
            us.append("</use>")
            us = "\n".join(us)
        if longdescription:
            ls = f"<longdescription>{longdescription}</longdescription>\n"

        sa = (
            ""
            if not stabilize_allarches
            else "<stabilize-allarches/>"
            if stabilize_allarches is True
            else f'<stabilize-allarches restrict="{stabilize_allarches}"/>'
        )
        sts = (
            ""
            if not straight_to_stable
            else "<straight-to-stable/>"
            if straight_to_stable is True
            else f'<straight-to-stable restrict="{straight_to_stable}"/>'
        )
        s = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE pkgmetadata SYSTEM "https://www.gentoo.org/dtd/metadata.dtd">
<pkgmetadata>
{cs}{ms}{us}{ls}{sa}{sts}{upstream}</pkgmetadata>"""
        return repo_objs.MetadataXml(data_source(s.encode("utf-8")))

    def test_empty_maintainers(self):
        assert () == self.get_metadata_xml().maintainers

    def test_maintainer_needed(self):
        mx = self.get_metadata_xml(comments=("<!-- maintainer-needed -->",))
        assert mx.maintainers == ()

    def test_multiple_maintainers(self):
        names = ("foo@gmail.com", "monkeybone@gmail.com")
        mx = self.get_metadata_xml(maintainers=tuple((x,) for x in names))
        assert sorted(names) == sorted(map(str, mx.maintainers))
        assert "foo@gmail.com" in mx.maintainers
        assert "monkeybone@gmail.com" in mx.maintainers

    def test_maintainer_name_with_email(self):
        mx = self.get_metadata_xml(
            maintainers=(("funkymonkey@gmail.com", "funky monkey \N{SNOWMAN}"),)
        )
        assert ("funky monkey \N{SNOWMAN} <funkymonkey@gmail.com>",) == tuple(
            map(str, mx.maintainers)
        )
        assert "funkymonkey@gmail.com" in mx.maintainers
        assert "funky monkey \N{SNOWMAN}" in mx.maintainers
        assert "funkymonkey@gmail.com" == mx.maintainers[0].email
        assert "funky monkey \N{SNOWMAN}" == mx.maintainers[0].name
        assert mx.maintainers[0].description is None
        assert mx.maintainers[0].maint_type is None
        assert mx.maintainers[0].proxied is None

    def test_maintainer_with_desc(self):
        mx = self.get_metadata_xml(maintainers=(("foo@bar.com", "foobar", "Foobar"),))
        assert ("foobar <foo@bar.com> (Foobar)",) == tuple(map(str, mx.maintainers))
        assert "foo@bar.com" in mx.maintainers
        assert "foobar" in mx.maintainers
        assert "foo@bar.com" == mx.maintainers[0].email
        assert "foobar" == mx.maintainers[0].name
        assert "Foobar" == mx.maintainers[0].description
        assert mx.maintainers[0].maint_type is None
        assert mx.maintainers[0].proxied is None

    def test_maintainer_with_type(self):
        mx = self.get_metadata_xml(
            maintainers=(("foo@bar.com", "foobar"),), maint_type="person"
        )
        assert ("foobar <foo@bar.com>",) == tuple(map(str, mx.maintainers))
        assert "foo@bar.com" in mx.maintainers
        assert "foobar" in mx.maintainers
        assert "foo@bar.com" == mx.maintainers[0].email
        assert "foobar" == mx.maintainers[0].name
        assert mx.maintainers[0].description is None
        assert "person" == mx.maintainers[0].maint_type
        assert mx.maintainers[0].proxied is None

    def test_maintainer_with_proxied(self):
        mx = self.get_metadata_xml(
            maintainers=(("foo@bar.com", "foobar"),), maint_type="person", proxied="yes"
        )
        assert ("foobar <foo@bar.com>",) == tuple(map(str, mx.maintainers))
        assert "foo@bar.com" in mx.maintainers
        assert "foobar" in mx.maintainers
        assert "foo@bar.com" == mx.maintainers[0].email
        assert "foobar" == mx.maintainers[0].name
        assert mx.maintainers[0].description is None
        assert "person" == mx.maintainers[0].maint_type
        assert "yes" == mx.maintainers[0].proxied

    def test_local_use(self):
        # empty...
        assert {} == self.get_metadata_xml().local_use

        local_use = {
            "foo": "description for foo",
            "bar": "description for bar (<pkg>app-foo/bar</pkg> required)",
        }
        metadata_xml = self.get_metadata_xml(local_use=local_use)
        pkg_tag_re = re.compile(r"</?pkg>")
        local_use = {k: pkg_tag_re.sub("", v) for k, v in local_use.items()}
        assert local_use == metadata_xml.local_use

    def test_longdesc(self):
        # empty...
        assert self.get_metadata_xml().longdescription is None
        s = """
I saw the best minds of my generation destroyed by madness, starving
hysterical naked, dragging themselves throughout the negro streets at dawn
looking for an angry fix, angle-headed hipsters burning for the ancient
heavenly connection to the starry dynamo in the machinery of night, who
poverty and tatters and hollowed-eyed and high sat up smoking in the
supernatural darkness of cold-water flats floating across the tops of cities
contemplating jazz, who bared their brains to Heaven under the El and saw
Mohammedan angels staggering on tenement roofs illuminated, who passed
through universities with radiant cool eyes hallucinating Arkansas and
Blake-light tragedy among the scholars of war.
"""

        assert (
            " ".join(s.split())
            == self.get_metadata_xml(longdescription=s).longdescription
        )

    def test_stabilize_allarches(self):
        # missing
        assert not self.get_metadata_xml().stabilize_allarches.value
        # present
        assert self.get_metadata_xml(stabilize_allarches=True).stabilize_allarches.value
        # with restriction
        x = self.get_metadata_xml(stabilize_allarches=">dev-lang/foo-1.0")
        assert x.stabilize_allarches.restrict == atom.atom(">dev-lang/foo-1.0")
        assert "dev-lang/foo-2" in x.stabilize_allarches
        assert "dev-lang/foo-1" not in x.stabilize_allarches
        # invalid restriction should be ignored
        x = self.get_metadata_xml(stabilize_allarches="invalid")
        assert x.stabilize_allarches.restrict is None

    def test_straight_to_stable(self):
        # missing
        assert not self.get_metadata_xml().straight_to_stable.value
        # present
        assert self.get_metadata_xml(straight_to_stable=True).straight_to_stable.value
        # with restriction
        x = self.get_metadata_xml(straight_to_stable=">dev-lang/foo-1.0")
        assert x.straight_to_stable.restrict == atom.atom(">dev-lang/foo-1.0")
        assert "dev-lang/foo-2" in x.straight_to_stable
        assert "dev-lang/foo-1" not in x.straight_to_stable
        # invalid restriction should be ignored
        x = self.get_metadata_xml(straight_to_stable="invalid")
        assert x.straight_to_stable.restrict is None

    def test_no_upstream(self):
        mx = self.get_metadata_xml()
        assert mx.upstream is None
        assert mx.upstreams == ()

    def test_empty_upstream(self):
        mx = self.get_metadata_xml(upstream="<upstream></upstream>")
        assert mx.upstream is not None
        assert mx.upstream.maintainers == ()
        assert mx.upstream.changelog is None
        assert mx.upstream.doc is None
        assert mx.upstream.bugs_to is None
        assert mx.upstream.remote_ids == ()
        assert mx.upstreams == ()

    def test_upstream_remote_ids(self):
        mx = self.get_metadata_xml(
            upstream="""<upstream>
            <remote-id type="github">foo/bar</remote-id>
            <remote-id type="pypi">baz</remote-id>
            </upstream>"""
        )
        assert mx.upstream.remote_ids == (
            repo_objs.Upstream("github", "foo/bar"),
            repo_objs.Upstream("pypi", "baz"),
        )
        # backwards compatible alias
        assert mx.upstreams == mx.upstream.remote_ids

    def test_upstream_links(self):
        mx = self.get_metadata_xml(
            upstream="""<upstream>
            <bugs-to>https://example.com/bugs</bugs-to>
            <changelog>https://example.com/changelog</changelog>
            <doc>https://example.com/doc</doc>
            </upstream>"""
        )
        assert mx.upstream.bugs_to == "https://example.com/bugs"
        assert mx.upstream.changelog == "https://example.com/changelog"
        assert mx.upstream.doc == "https://example.com/doc"

    def test_upstream_doc_lang(self):
        mx = self.get_metadata_xml(
            upstream="""<upstream>
            <doc lang="de">https://example.com/de</doc>
            <doc>https://example.com/en</doc>
            </upstream>"""
        )
        assert mx.upstream.doc == "https://example.com/en"

    def test_upstream_maintainers(self):
        mx = self.get_metadata_xml(
            upstream="""<upstream>
            <maintainer status="active">
                <name>Foo Bar</name>
                <email>foo@example.com</email>
            </maintainer>
            <maintainer status="inactive">
                <name>Baz</name>
            </maintainer>
            </upstream>"""
        )
        assert mx.upstream.maintainers == (
            repo_objs.UpstreamMaintainer("Foo Bar", "foo@example.com", "active"),
            repo_objs.UpstreamMaintainer("Baz", None, "inactive"),
        )


class TestProjectsXml:
    @staticmethod
    def get_projects_xml(s=None):
        default_s = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE projects SYSTEM "https://www.gentoo.org/dtd/projects.dtd">
<projects>
  <project>
    <email>nomembers@example.com</email>
    <name>Project with no members</name>
    <url>https://projects.example.com/nomembers</url>
    <description>Here is the description</description>
  </project>
  <project>
    <email>nolead@example.com</email>
    <name>Project with no lead</name>
    <url>https://projects.example.com/nolead</url>
    <description>Here is the description</description>
    <member>
      <email>fulldev1@example.com</email>
      <name>Full Dev</name>
      <role>Somebody</role>
    </member>
    <member>
      <email>dev1@example.com</email>
    </member>
  </project>
  <project>
    <email>lead@example.com</email>
    <name>Project with a lead</name>
    <url>https://projects.example.com/lead</url>
    <description>Here is the description</description>
    <member>
      <email>fulldev2@example.com</email>
      <name>Full Dev</name>
      <role>Somebody</role>
    </member>
    <member is-lead="1">
      <email>dev2@example.com</email>
    </member>
  </project>
  <project>
    <email>subprojects@example.com</email>
    <name>Project with non-recursive subprojects</name>
    <url>https://projects.example.com/subprojects</url>
    <description>Here is the description</description>
    <member>
      <email>fulldev3@example.com</email>
      <name>Full Dev</name>
      <role>Somebody</role>
    </member>
    <member is-lead="1">
      <email>dev3@example.com</email>
    </member>
    <subproject ref="nolead@example.com"/>
    <subproject ref="lead@example.com"/>
  </project>
  <project>
    <email>recursive-subprojects@example.com</email>
    <name>Project with recursive subprojects</name>
    <url>https://projects.example.com/recursive-subprojects</url>
    <description>Here is the description</description>
    <member>
      <email>fulldev4@example.com</email>
      <name>Full Dev</name>
      <role>Somebody</role>
    </member>
    <member>
      <!-- this one is common -->
      <email>dev1@example.com</email>
    </member>
    <member is-lead="1">
      <email>dev4@example.com</email>
    </member>
    <subproject ref="nolead@example.com" inherit-members="1"/>
    <subproject ref="lead@example.com"/>
  </project>
  <project>
    <email>super-recursive-subprojects@example.com</email>
    <name>Project with double recursion</name>
    <url>https://projects.example.com/super-recursive-subprojects</url>
    <description>Here is the description</description>
    <member>
      <email>fulldev5@example.com</email>
      <name>Full Dev</name>
      <role>Somebody</role>
    </member>
    <member is-lead="1">
      <!-- this one is common -->
      <email>dev2@example.com</email>
    </member>
    <member>
      <email>dev5@example.com</email>
    </member>
    <subproject ref="recursive-subprojects@example.com" inherit-members="1"/>
  </project>
</projects>"""
        if s is None:
            s = default_s
        return repo_objs.ProjectsXml(data_source(s.encode("utf-8")))

    def test_empty(self):
        assert self.get_projects_xml("").projects == {}

    def test_invalid_xml(self):
        assert self.get_projects_xml("<foo><bar></foo>").projects == {}

    def test_project_member_without_email(self):
        p = self.get_projects_xml(
            """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE projects SYSTEM "https://www.gentoo.org/dtd/projects.dtd">
<projects>
  <project>
    <email>nolead@example.com</email>
    <name>Project with no lead</name>
    <url>https://projects.example.com/nolead</url>
    <description>Here is the description</description>
    <member>
      <name>Full Dev</name>
      <role>Somebody</role>
    </member>
  </project>
</projects>
"""
        ).projects["nolead@example.com"]
        assert p.members == ()

    def test_subproject_without_ref(self):
        p = self.get_projects_xml(
            """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE projects SYSTEM "https://www.gentoo.org/dtd/projects.dtd">
<projects>
  <project>
    <email>nolead@example.com</email>
    <name>Project with no lead</name>
    <url>https://projects.example.com/nolead</url>
    <description>Here is the description</description>
    <member>
      <email>fulldev1@example.com</email>
      <name>Full Dev</name>
      <role>Somebody</role>
    </member>
    <subproject inherit-members="1"/>
  </project>
</projects>
"""
        ).projects["nolead@example.com"]
        assert p.subprojects == ()

    def test_subproject_with_invalid_ref(self):
        p = self.get_projects_xml(
            """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE projects SYSTEM "https://www.gentoo.org/dtd/projects.dtd">
<projects>
  <project>
    <email>nolead@example.com</email>
    <name>Project with no lead</name>
    <url>https://projects.example.com/nolead</url>
    <description>Here is the description</description>
    <member>
      <email>fulldev1@example.com</email>
      <name>Full Dev</name>
      <role>Somebody</role>
    </member>
    <subproject ref="nonexist@example.com" inherit-members="1"/>
  </project>
</projects>
"""
        ).projects["nolead@example.com"]
        assert p.subprojects[0].project is None
        assert p.recursive_members == p.members

    def test_deep_subproject_with_invalid_ref(self):
        p = self.get_projects_xml(
            """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE projects SYSTEM "https://www.gentoo.org/dtd/projects.dtd">
<projects>
  <project>
    <email>nolead@example.com</email>
    <name>Project with no lead</name>
    <url>https://projects.example.com/nolead</url>
    <description>Here is the description</description>
    <member>
      <email>fulldev1@example.com</email>
      <name>Full Dev</name>
      <role>Somebody</role>
    </member>
    <subproject ref="nonexist@example.com" inherit-members="1"/>
  </project>
  <project>
    <email>subprojects@example.com</email>
    <name>Project with real subprojects</name>
    <url>https://projects.example.com/subprojects</url>
    <description>Here is the description</description>
    <subproject ref="nolead@example.com" inherit-members="1"/>
  </project>
</projects>
"""
        ).projects
        assert (
            p["subprojects@example.com"].recursive_members
            == p["nolead@example.com"].members
        )

    def test_basic_metadata(self):
        projects = self.get_projects_xml().projects
        for email, project in projects.items():
            assert project.email == email
            assert project.name.startswith("Project with")
            assert project.url == (
                "https://projects.example.com/" + email.split("@")[0]
            )
            assert project.description == "Here is the description"

    def test_no_members(self):
        p = self.get_projects_xml().projects["nomembers@example.com"]
        assert p.members == ()
        assert p.leads == ()
        assert p.subprojects == ()
        assert p.recursive_members == ()

    def test_no_lead(self):
        p = self.get_projects_xml().projects["nolead@example.com"]
        assert p.members[0].email == "fulldev1@example.com"
        assert p.members[0].name == "Full Dev"
        assert p.members[0].role == "Somebody"
        assert not p.members[0].is_lead
        assert p.members[1].email == "dev1@example.com"
        assert p.members[1].name is None
        assert p.members[1].role is None
        assert not p.members[1].is_lead
        assert p.leads == ()
        assert p.subprojects == ()
        assert p.recursive_members == p.members

    def test_have_lead(self):
        p = self.get_projects_xml().projects["lead@example.com"]
        assert p.members[0].email == "fulldev2@example.com"
        assert p.members[0].name == "Full Dev"
        assert p.members[0].role == "Somebody"
        assert not p.members[0].is_lead
        assert p.members[1].email == "dev2@example.com"
        assert p.members[1].name is None
        assert p.members[1].role is None
        assert p.members[1].is_lead
        assert p.leads == (p.members[1],)
        assert p.subprojects == ()
        assert p.recursive_members == p.members

    def test_subprojects(self):
        p = self.get_projects_xml().projects["subprojects@example.com"]
        assert p.subprojects[0].email == "nolead@example.com"
        assert p.subprojects[0].name == "Project with no lead"
        assert not p.subprojects[0].inherit_members
        assert p.subprojects[1].email == "lead@example.com"
        assert p.subprojects[1].name == "Project with a lead"
        assert not p.subprojects[1].inherit_members
        assert p.recursive_members == p.members

    @staticmethod
    def unlead(members):
        for m in members:
            yield repo_objs.ProjectMember(
                email=m.email, name=m.name, role=m.role, is_lead=False
            )

    def test_recursive_subprojects(self):
        projects = self.get_projects_xml().projects
        p = projects["recursive-subprojects@example.com"]
        sp = projects["nolead@example.com"]
        assert p.subprojects[0].email == "nolead@example.com"
        assert p.subprojects[0].name == "Project with no lead"
        assert p.subprojects[0].inherit_members
        assert p.subprojects[1].email == "lead@example.com"
        assert p.subprojects[1].name == "Project with a lead"
        assert not p.subprojects[1].inherit_members
        # one extra member should be inherited from sp
        assert p.recursive_members == (p.members + tuple(self.unlead(sp.members[:1])))
        # lead of sp should not be considered lead of p
        assert not p.recursive_members[3].is_lead

    def test_super_recursive_subprojects(self):
        projects = self.get_projects_xml().projects
        p = projects["super-recursive-subprojects@example.com"]
        sp = projects["recursive-subprojects@example.com"]
        ssp = projects["nolead@example.com"]
        assert p.subprojects[0].email == "recursive-subprojects@example.com"
        assert p.subprojects[0].name == "Project with recursive subprojects"
        assert p.subprojects[0].inherit_members
        # one extra member should be inherited from ssp
        assert p.recursive_members == (
            p.members + tuple(self.unlead(sp.members + ssp.members[:1]))
        )
        # lead of sp should not be considered lead of p
        assert not p.recursive_members[5].is_lead


class TestRepoConfig:
    @pytest.fixture(autouse=True)
    def _setup(self, tmpdir):
        self.repo_path = str(tmpdir)
        self.profiles_base = os.path.join(self.repo_path, "profiles")
        self.metadata_path = os.path.join(
            self.repo_path, repo_objs.RepoConfig.layout_offset
        )
        self.eapi_path = os.path.join(self.profiles_base, "eapi")

    def test_nonexistent_repo(self):
        # Newly configured, nonexistent repos shouldn't cause issues.
        repo_config = repo_objs.RepoConfig("nonexistent")
        assert repo_config.location == "nonexistent"

    def test_default_eapi(self):
        os.mkdir(self.profiles_base)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert str(repo_config.eapi) == "0"

    def test_empty_file_eapi(self):
        os.mkdir(self.profiles_base)
        touch(self.eapi_path)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert str(repo_config.eapi) == "0"

    def test_empty_content_eapi(self):
        os.mkdir(self.profiles_base)
        with open(self.eapi_path, "w+") as f:
            f.write("     \n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert str(repo_config.eapi) == "0"

    def test_unknown_eapi(self):
        os.mkdir(self.profiles_base)
        with open(self.eapi_path, "w+") as f:
            f.write("unknown_eapi")
        with pytest.raises(repo_errors.UnsupportedRepo) as excinfo:
            repo_objs.RepoConfig(self.repo_path)
        assert isinstance(excinfo.value.repo, repo_objs.RepoConfig)

    def test_known_eapi(self):
        os.mkdir(self.profiles_base)
        with open(self.eapi_path, "w+") as f:
            f.write("6")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert str(repo_config.eapi) == "6"

    def test_bad_data_known_eapi(self, caplog):
        os.mkdir(self.profiles_base)
        with open(self.eapi_path, "w+") as f:
            f.write("4\nfoo\nbar")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert str(repo_config.eapi) == "4"
        assert "multiple lines detected" in caplog.text

    def test_bad_data_unknown_eapi(self, caplog):
        os.mkdir(self.profiles_base)
        with open(self.eapi_path, "w+") as f:
            f.write("eapi\nfoo\nbar")
        with pytest.raises(repo_errors.UnsupportedRepo) as excinfo:
            repo_objs.RepoConfig(self.repo_path)
        assert isinstance(excinfo.value.repo, repo_objs.RepoConfig)
        assert "multiple lines detected" in caplog.text

    def test_is_empty(self, caplog):
        caplog.set_level(logging.DEBUG)

        # nonexistent repo
        repo_config = repo_objs.RepoConfig("nonexistent")
        assert repo_config.is_empty
        assert caplog.text == ""
        caplog.clear()

        # empty repo
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.is_empty
        assert "repo is empty:" in caplog.text
        caplog.clear()

        # profiles dir exists
        os.mkdir(self.profiles_base)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert not repo_config.is_empty

    def test_repo_name(self, caplog):
        # nonexistent file
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_name is None

        # empty file
        os.mkdir(os.path.dirname(self.metadata_path))
        touch(self.metadata_path)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_name is None

        # bad data formatting
        with open(self.metadata_path, "w") as f:
            f.write("repo-name repo")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_name is None
        assert "bash parse error" in caplog.text
        caplog.clear()

        # bad data formatting + name
        with open(self.metadata_path, "w") as f:
            f.write("foo bar\nrepo-name = repo0")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_name == "repo0"
        assert "bash parse error" in caplog.text
        caplog.clear()

        # unset
        with open(self.metadata_path, "w") as f:
            f.write("repo-name =")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_name == ""

        # whitespace
        with open(self.metadata_path, "w") as f:
            f.write("repo-name =  \n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_name == ""

        # whitespace + name
        with open(self.metadata_path, "w") as f:
            f.write("repo-name = repo \n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_name == "repo"

        # regular name
        with open(self.metadata_path, "w") as f:
            f.write("repo-name = repo1")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_name == "repo1"

    def test_manifests(self):
        # nonexistent file
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.manifests == {
            "disabled": False,
            "strict": True,
            "thin": False,
            "signed": True,
            "hashes": repo_objs.RepoConfig.default_hashes,
            "required_hashes": repo_objs.RepoConfig.default_required_hashes,
        }

        # regular data
        os.mkdir(os.path.dirname(self.metadata_path))
        with open(self.metadata_path, "w") as f:
            f.write("manifest-hashes = foo\n")
            f.write("manifest-required-hashes = bar\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.manifests.hashes == ("size", "foo")
        assert repo_config.manifests.required_hashes == ("size", "bar")

    def test_masters(self, caplog):
        # empty repo
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.masters == ()
        assert caplog.text == ""
        caplog.clear()

        # nonempty repo
        os.mkdir(self.profiles_base)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.masters == ()
        assert "doesn't specify masters in metadata" in caplog.text
        caplog.clear()

        # explicit empty masters for standalone repo
        os.mkdir(os.path.dirname(self.metadata_path))
        with open(self.metadata_path, "w") as f:
            f.write("masters =\n")
        repo_config = repo_objs.RepoConfig(self.repo_path, config_name="foo")
        assert repo_config.masters == ()
        assert caplog.text == ""
        caplog.clear()

        # overlay repo with masters
        with open(self.metadata_path, "w") as f:
            f.write("masters = foo bar\n")
        repo_config = repo_objs.RepoConfig(self.repo_path, config_name="a")
        assert repo_config.masters == ("foo", "bar")

        # overlay repo with duplicate masters
        with open(self.metadata_path, "w") as f:
            f.write("masters = foo bar foo baz\n")
        repo_config = repo_objs.RepoConfig(self.repo_path, config_name="b")
        assert repo_config.masters == ("foo", "bar", "baz")

    def test_cache_format(self, caplog):
        # empty repo
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.cache_format == "md5-dict"

        # explicit empty setting
        os.mkdir(os.path.dirname(self.metadata_path))
        with open(self.metadata_path, "w") as f:
            f.write("cache-formats =\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.cache_format is None

        # unknown formats
        with open(self.metadata_path, "w") as f:
            f.write("cache-formats = foo bar\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.cache_format == "md5-dict"
        assert "unknown cache format:" in caplog.text
        caplog.clear()

        # known format
        with open(self.metadata_path, "w") as f:
            f.write("cache-formats = pms\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.cache_format == "pms"

        # multiple formats -- favored format is selected
        with open(self.metadata_path, "w") as f:
            f.write("cache-formats = pms md5-dict\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.cache_format == "md5-dict"

        # unknown + known
        with open(self.metadata_path, "w") as f:
            f.write("cache-formats = foo md5-dict\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.cache_format == "md5-dict"

    def test_profile_formats(self, caplog):
        os.mkdir(self.profiles_base)
        with open(os.path.join(self.profiles_base, "repo_name"), "w") as f:
            f.write("pms_name")

        # empty repo
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.profile_formats == {"pms"}
        caplog.clear()

        # explicit empty setting
        os.mkdir(os.path.dirname(self.metadata_path))
        with open(self.metadata_path, "w") as f:
            f.write("masters =\nprofile-formats =\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.profile_formats == {"pms"}
        assert not caplog.text
        caplog.clear()

        # message shown at info log level
        caplog.set_level(logging.INFO)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert "has explicitly unset profile-formats" in caplog.text
        caplog.clear()

        # unknown formats
        caplog.set_level(logging.WARNING)
        with open(self.metadata_path, "w") as f:
            f.write("masters =\nprofile-formats = foo bar\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.profile_formats == {"pms"}
        assert not caplog.text
        caplog.clear()

        # message shown at info log level
        caplog.set_level(logging.INFO)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert "has unsupported profile format" in caplog.text
        caplog.clear()

        # unknown + known
        caplog.set_level(logging.WARNING)
        with open(self.metadata_path, "w") as f:
            f.write("masters =\nprofile-formats = foo portage-2\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.profile_formats == {"pms", "portage-2"}
        assert not caplog.text
        caplog.clear()

        # message shown at info log level
        caplog.set_level(logging.INFO)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert "has unsupported profile format" in caplog.text
        caplog.clear()

        # known formats
        caplog.set_level(logging.WARNING)
        with open(self.metadata_path, "w") as f:
            f.write("profile-formats = portage-1 portage-2\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.profile_formats == {"portage-1", "portage-2"}

    def test_pms_repo_name(self):
        os.mkdir(self.profiles_base)
        repo_name_path = os.path.join(self.profiles_base, "repo_name")

        # nonexistent file
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.pms_repo_name is None

        # empty file
        touch(repo_name_path)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.pms_repo_name == ""

        # whitespace
        with open(repo_name_path, "w") as f:
            f.write(" \n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.pms_repo_name == ""

        # whitespace + name
        with open(repo_name_path, "w") as f:
            f.write(" repo \n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.pms_repo_name == "repo"

        # regular name
        with open(repo_name_path, "w") as f:
            f.write("newrepo")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.pms_repo_name == "newrepo"

        # regular name EOLed
        with open(repo_name_path, "w") as f:
            f.write("newrepo2\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.pms_repo_name == "newrepo2"

        # multi-line
        with open(repo_name_path, "w") as f:
            f.write("newrepo3\nfoobar")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.pms_repo_name == "newrepo3"

        # binary data
        with open(repo_name_path, "wb") as f:
            f.write(b"\x6e\x65\x77\x72\x65\x70\x6f\x34")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.pms_repo_name == "newrepo4"

    def test_repo_id(self, caplog):
        # nonexistent repo
        repo_config = repo_objs.RepoConfig("nonexistent")
        assert repo_config.repo_id == "nonexistent"

        # empty repo
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_id == self.repo_path
        assert caplog.text == ""
        caplog.clear()

        # nonempty repo
        os.mkdir(self.profiles_base)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_id == self.repo_path
        assert "repo lacks a defined name:" in caplog.text
        caplog.clear()

        # pms repo name exists
        with open(os.path.join(self.profiles_base, "repo_name"), "w") as f:
            f.write("pms_name")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_id == "pms_name"

        # layout.conf repo name exists
        os.mkdir(os.path.dirname(self.metadata_path))
        with open(self.metadata_path, "w") as f:
            f.write("repo-name = metadata_name")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.repo_id == "metadata_name"

        # config name exists
        repo_config = repo_objs.RepoConfig(self.repo_path, config_name="config_name")
        assert repo_config.repo_id == "config_name"

    def test_known_arches(self):
        # nonexistent repo
        repo_config = repo_objs.RepoConfig("nonexistent")
        assert repo_config.known_arches == frozenset()

        # empty file
        os.mkdir(self.profiles_base)
        arches_path = os.path.join(self.profiles_base, "arch.list")
        touch(arches_path)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.known_arches == frozenset()

        # single entry
        with open(arches_path, "w") as f:
            f.write("foo")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.known_arches == frozenset(["foo"])

        # multiple entries with whitespaces and comments
        with open(arches_path, "w") as f:
            f.write(
                """
                amd64
                x86

                # prefix
                foo-bar
                """
            )
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.known_arches == frozenset(["amd64", "x86", "foo-bar"])

    def test_arches_desc(self):
        # nonexistent repo
        repo_config = repo_objs.RepoConfig("nonexistent")
        empty = {"stable": set(), "transitional": set(), "testing": set()}
        assert repo_config.arches_desc == ImmutableDict(empty)

        # empty file
        os.mkdir(self.profiles_base)
        arches_desc_path = os.path.join(self.profiles_base, "arches.desc")
        touch(arches_desc_path)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.arches_desc == ImmutableDict(empty)

        # regular entries
        with open(os.path.join(self.profiles_base, "arch.list"), "w") as f:
            f.write(
                """
                amd64
                alpha
                foo
                """
            )
        with open(arches_desc_path, "w") as f:
            f.write(
                """
                # arches.desc file

                amd64 stable
                alpha testing
                """
            )
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.arches_desc["stable"] == {"amd64"}
        assert repo_config.arches_desc["testing"] == {"alpha"}
        assert repo_config.arches_desc["transitional"] == set()

    def test_use_desc(self):
        # nonexistent repo
        repo_config = repo_objs.RepoConfig("nonexistent")
        assert repo_config.use_desc == ()

        # empty file
        os.mkdir(self.profiles_base)
        use_desc_path = os.path.join(self.profiles_base, "use.desc")
        touch(use_desc_path)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.use_desc == ()

        # regular entries
        with open(use_desc_path, "w") as f:
            f.write(
                """
                # copy
                # license

                foo1 - enable foo1
                foo2 - enable foo2
                bar3 - add bar3 support
                """
            )
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert 3 == len(repo_config.use_desc)

    def test_use_expand_desc(self):
        # nonexistent repo
        repo_config = repo_objs.RepoConfig("nonexistent")
        assert repo_config.use_expand_desc == {}

        # empty file
        use_expand_desc_path = os.path.join(self.profiles_base, "desc")
        os.makedirs(use_expand_desc_path)
        use_expand_desc_file = os.path.join(use_expand_desc_path, "foo.desc")
        touch(use_expand_desc_file)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.use_expand_desc == {"foo": ()}

        # regular entries
        with open(use_expand_desc_file, "w") as f:
            f.write(
                """
                # copy
                # license

                bar - add bar support
                baz - build using baz
                """
            )
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.use_expand_desc == {
            "foo": (("foo_bar", "add bar support"), ("foo_baz", "build using baz"))
        }

    def test_use_local_desc(self):
        # nonexistent repo
        repo_config = repo_objs.RepoConfig("nonexistent")
        assert repo_config.use_local_desc == ()

        # empty file
        os.mkdir(self.profiles_base)
        use_local_desc_path = os.path.join(self.profiles_base, "use.local.desc")
        touch(use_local_desc_path)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.use_local_desc == ()

        # regular entries
        with open(use_local_desc_path, "w") as f:
            f.write(
                """
                # copy
                # license

                cat/pkg1:foo1 - enable foo1
                cat1/pkg2:foo2 - enable foo2
                cat2/pkg3:bar3 - add bar3 support
                """
            )
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert 3 == len(repo_config.use_local_desc)

    def test_updates(self):
        # nonexistent repo
        repo_config = repo_objs.RepoConfig("nonexistent")
        assert repo_config.updates == {}

        # empty file
        updates_path = os.path.join(self.profiles_base, "updates")
        updates_file_path = os.path.join(updates_path, "1Q-2019")
        os.makedirs(updates_path)
        touch(updates_file_path)
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.updates == {}

        # simple pkg move
        # TODO: move pkg_updates content tests to its own module
        with open(updates_file_path, "w") as f:
            f.write("move cat1/pkg1 cat2/pkg1\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        expected_updates = {
            "cat1/pkg1": [("move", atom.atom("cat1/pkg1"), atom.atom("cat2/pkg1"))],
        }
        assert repo_config.updates == expected_updates

        # extraneous file should be ignored
        extra_file_path = os.path.join(updates_path, "frobnicate")
        with open(extra_file_path, "w") as f:
            f.write("move cat1/pkg2 cat1/pkg3\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.updates == expected_updates

    def test_updates_eapi8(self):
        # empty file
        updates_path = os.path.join(self.profiles_base, "updates")
        updates_file_path = os.path.join(updates_path, "2021.1")
        os.makedirs(updates_path)
        touch(updates_file_path)
        with open(os.path.join(self.profiles_base, "eapi"), "w") as f:
            f.write("8\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.updates == {}

        # simple pkg move
        # TODO: move pkg_updates content tests to its own module
        with open(updates_file_path, "w") as f:
            f.write("move cat1/pkg1 cat2/pkg1\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        expected_updates = {
            "cat1/pkg1": [("move", atom.atom("cat1/pkg1"), atom.atom("cat2/pkg1"))],
        }
        assert repo_config.updates == expected_updates

        # extraneous file should be ignored
        extra_file_path = os.path.join(updates_path, ".frobnicate")
        with open(extra_file_path, "w") as f:
            f.write("move cat1/pkg2 cat1/pkg3\n")
        repo_config = repo_objs.RepoConfig(self.repo_path)
        assert repo_config.updates == expected_updates
