# -*- coding: utf-8 -*-
# Tests for samba-tool visualize
# Copyright (C) Andrew Bartlett 2015, 2018
#
# by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
"""Tests for samba-tool visualize ntdsconn using the test ldif
topologies.

We don't test samba-tool visualize reps here because repsTo and
repsFrom are not replicated, and there are no actual remote servers to
query.
"""
import os
import tempfile
import re
from io import StringIO
from samba.tests.samba_tool.base import SambaToolCmdTest
from samba.kcc import ldif_import_export
from samba.graph import COLOUR_SETS
from samba.param import LoadParm

MULTISITE_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
                              "testdata/ldif-utils-test-multisite.ldif")

# UNCONNECTED_LDIF is a single site, unconnected 5DC database that was
# created using samba-tool domain join in testenv.
UNCONNECTED_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
                                "testdata/unconnected-intrasite.ldif")

DOMAIN = "DC=ad,DC=samba,DC=example,DC=com"
DN_TEMPLATE = "CN=%s,CN=Servers,CN=%s,CN=Sites,CN=Configuration," + DOMAIN

MULTISITE_LDIF_DSAS = [
    ("WIN01", "Default-First-Site-Name"),
    ("WIN08", "Site-4"),
    ("WIN07", "Site-4"),
    ("WIN06", "Site-3"),
    ("WIN09", "Site-5"),
    ("WIN10", "Site-5"),
    ("WIN02", "Site-2"),
    ("WIN04", "Site-2"),
    ("WIN03", "Site-2"),
    ("WIN05", "Site-2"),
]


class StringIOThinksItIsATTY(StringIO):
    """A StringIO that claims to be a TTY for testing --color=auto,
    by switching the stringIO class attribute."""
    def isatty(self):
        return True


def samdb_from_ldif(ldif, tempdir, lp, dsa=None, tag=''):
    if dsa is None:
        dsa_name = 'default-DSA'
    else:
        dsa_name = dsa[:5]
    dburl = os.path.join(tempdir,
                         ("ldif-to-sambdb-%s-%s" %
                          (tag, dsa_name)))
    samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif,
                                             forced_local_dsa=dsa)
    return (samdb, dburl)


def collapse_space(s, keep_empty_lines=False):
    lines = []
    for line in s.splitlines():
        line = ' '.join(line.strip().split())
        if line or keep_empty_lines:
            lines.append(line)
    return '\n'.join(lines)


class SambaToolVisualizeLdif(SambaToolCmdTest):
    def setUp(self):
        super().setUp()
        self.lp = LoadParm()
        self.samdb, self.dbfile = samdb_from_ldif(MULTISITE_LDIF,
                                                  self.tempdir,
                                                  self.lp)
        self.dburl = 'tdb://' + self.dbfile

    def tearDown(self):
        self.remove_files(self.dbfile)
        super().tearDown()

    def remove_files(self, *files):
        for f in files:
            self.assertTrue(f.startswith(self.tempdir))
            os.unlink(f)

    def test_colour(self):
        """Ensure the colour output is the same as the monochrome output
        EXCEPT for the colours, of which the monochrome one should
        know nothing."""
        colour_re = re.compile('\033' r'\[[\d;]+m')
        result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
                                                 '-H', self.dburl,
                                                 '--color=no', '-S')
        self.assertCmdSuccess(result, monochrome, err)
        self.assertFalse(colour_re.findall(monochrome))

        colour_args = [['--color=yes']]
        colour_args += [['--color-scheme', x] for x in COLOUR_SETS
                        if x is not None]

        for args in colour_args:
            result, out, err = self.runsubcmd("visualize", "ntdsconn",
                                              '-H', self.dburl,
                                              '-S', *args)
            self.assertCmdSuccess(result, out, err)
            self.assertTrue(colour_re.search(out),
                            f"'{' '.join(args)}' should be colour")
            uncoloured = colour_re.sub('', out)

            self.assertStringsEqual(monochrome, uncoloured, strip=True)

    def assert_colour(self, text, has_colour=True, monochrome=None):
        colour_re = re.compile('\033' r'\[[\d;]+m')
        found = colour_re.search(text)
        if has_colour:
            self.assertTrue(found, text)
        else:
            self.assertFalse(found, text)
        if monochrome is not None:
            uncoloured = colour_re.sub('', text)
            self.assertStringsEqual(monochrome, uncoloured, strip=True)

    def test_colour_auto_tty(self):
        """Assert the behaviour of --colour=auto with and without
        NO_COLOUR on a fake tty"""
        result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
                                                 '-H', self.dburl,
                                                 '--color=no', '-S')
        self.assertCmdSuccess(result, monochrome, err)
        self.assert_colour(monochrome, False)
        cls = self.__class__

        try:
            cls.stringIO = StringIOThinksItIsATTY
            old_no_color = os.environ.pop('NO_COLOR', None)
            # First with no NO_COLOR env var. There should be colour.
            result, out, err = self.runsubcmd("visualize", "ntdsconn",
                                              '-H', self.dburl,
                                              '-S',
                                              '--color=auto')
            self.assertCmdSuccess(result, out, err)
            self.assert_colour(out, True, monochrome)

            for env, opt, is_colour in [
                    # NO_COLOR='' should be as if no NO_COLOR
                    ['', '--color=auto', True],
                    # NO_COLOR='1': we expect no colour
                    ['1', '--color=auto', False],
                    # NO_COLOR='no': we still expect no colour
                    ['no', '--color=auto', False],
                    # NO_COLOR=' ', alias for 'auto'
                    [' ', '--color=tty', False],
                    # NO_COLOR=' ', alias for 'auto'
                    [' ', '--color=if-tty', False],
                    # NO_COLOR='', alias for 'auto'
                    ['', '--color=tty', True],
                    # NO_COLOR='', alias for 'no'
                    ['', '--color=never', False],
                    # NO_COLOR='x', alias for 'yes' (--color=yes wins)
                    ['x', '--color=force', True],
                    ]:
                os.environ['NO_COLOR'] = env

                try:
                    result, out, err = self.runsubcmd("visualize", "ntdsconn",
                                                      '-H', self.dburl,
                                                      '-S',
                                                      opt)
                except SystemExit:
                    # optparse makes us do this
                    self.fail(f"optparse rejects {env}, {opt}, {is_colour}")

                self.assertCmdSuccess(result, out, err)
                self.assert_colour(out, is_colour, monochrome)

                # with "-o -" output filename alias for stdout.
                result, out, err = self.runsubcmd("visualize", "ntdsconn",
                                                  '-H', self.dburl,
                                                  '-S',
                                                  opt,
                                                  '-o', '-')
                self.assertCmdSuccess(result, out, err)
                self.assert_colour(out, is_colour, monochrome)

        finally:
            cls.stringIO = StringIO
            if old_no_color is None:
                os.environ.pop('NO_COLOR', None)
            else:
                os.environ['NO_COLOR'] = old_no_color

    def test_import_ldif_xdot(self):
        """We can't test actual xdot, but using the environment we can
        persuade samba-tool that a script we write is xdot and ensure
        it gets the right text.
        """
        result, expected, err = self.runsubcmd("visualize", "ntdsconn",
                                               '-H', self.dburl,
                                               '--color=no', '-S',
                                               '--dot')
        self.assertCmdSuccess(result, expected, err)

        # not that we're expecting anything here
        old_xdot_path = os.environ.get('SAMBA_TOOL_XDOT_PATH')

        tmpdir = tempfile.mkdtemp()
        fake_xdot = os.path.join(tmpdir, 'fake_xdot')
        content = os.path.join(tmpdir, 'content')
        f = open(fake_xdot, 'w')
        print('#!/bin/sh', file=f)
        print('cp $1 %s' % content, file=f)
        f.close()
        os.chmod(fake_xdot, 0o700)

        os.environ['SAMBA_TOOL_XDOT_PATH'] = fake_xdot
        result, empty, err = self.runsubcmd("visualize", "ntdsconn",
                                            '--importldif', MULTISITE_LDIF,
                                            '--color=no', '-S',
                                            '--xdot')

        f = open(content)
        xdot = f.read()
        f.close()
        os.remove(fake_xdot)
        os.remove(content)
        os.rmdir(tmpdir)

        if old_xdot_path is not None:
            os.environ['SAMBA_TOOL_XDOT_PATH'] = old_xdot_path
        else:
            del os.environ['SAMBA_TOOL_XDOT_PATH']

        self.assertCmdSuccess(result, xdot, err)
        self.assertStringsEqual(expected, xdot, strip=True)

    def test_import_ldif(self):
        """Make sure the samba-tool visualize --importldif option gives the
        same output as using the externally generated db from the same
        LDIF."""
        result, s1, err = self.runsubcmd("visualize", "ntdsconn",
                                         '-H', self.dburl,
                                         '--color=no', '-S')
        self.assertCmdSuccess(result, s1, err)

        result, s2, err = self.runsubcmd("visualize", "ntdsconn",
                                         '--importldif', MULTISITE_LDIF,
                                         '--color=no', '-S')
        self.assertCmdSuccess(result, s2, err)

        self.assertStringsEqual(s1, s2)

    def test_output_file(self):
        """Check that writing to a file works, with and without
        --color=auto."""
        # NOTE, we can't really test --color=auto works with a TTY.
        colour_re = re.compile('\033' r'\[[\d;]+m')
        result, expected, err = self.runsubcmd("visualize", "ntdsconn",
                                               '-H', self.dburl,
                                               '--color=auto', '-S')
        self.assertCmdSuccess(result, expected, err)
        # Not a TTY, so stdout output should be colourless
        self.assertFalse(colour_re.search(expected))
        expected = expected.strip()

        color_auto_file = os.path.join(self.tempdir, 'color-auto')

        result, out, err = self.runsubcmd("visualize", "ntdsconn",
                                          '-H', self.dburl,
                                          '--color=auto', '-S',
                                          '-o', color_auto_file)
        self.assertCmdSuccess(result, out, err)
        # We wrote to file, so stdout should be empty
        self.assertEqual(out, '')
        f = open(color_auto_file)
        color_auto = f.read()
        f.close()
        self.assertStringsEqual(color_auto, expected, strip=True)
        self.remove_files(color_auto_file)

        color_no_file = os.path.join(self.tempdir, 'color-no')
        result, out, err = self.runsubcmd("visualize", "ntdsconn",
                                          '-H', self.dburl,
                                          '--color=no', '-S',
                                          '-o', color_no_file)
        self.assertCmdSuccess(result, out, err)
        self.assertEqual(out, '')
        f = open(color_no_file)
        color_no = f.read()
        f.close()
        self.remove_files(color_no_file)

        self.assertStringsEqual(color_no, expected, strip=True)

        color_yes_file = os.path.join(self.tempdir, 'color-yes')
        result, out, err = self.runsubcmd("visualize", "ntdsconn",
                                          '-H', self.dburl,
                                          '--color=yes', '-S',
                                          '-o', color_yes_file)
        self.assertCmdSuccess(result, out, err)
        self.assertEqual(out, '')
        f = open(color_yes_file)
        colour_yes = f.read()
        f.close()
        self.assertNotEqual(colour_yes.strip(), expected)

        self.remove_files(color_yes_file)

        # Try the magic filename "-", meaning stdout.
        # This doesn't exercise the case when stdout is a TTY
        for c, equal in [('no', True), ('auto', True), ('yes', False)]:
            result, out, err = self.runsubcmd("visualize", "ntdsconn",
                                              '-H', self.dburl,
                                              '--color', c,
                                              '-S', '-o', '-')
            self.assertCmdSuccess(result, out, err)
            self.assertEqual((out.strip() == expected), equal)

    def test_utf8(self):
        """Ensure that --utf8 adds at least some expected utf-8, and that it
        isn't there without --utf8."""
        result, utf8, err = self.runsubcmd("visualize", "ntdsconn",
                                           '-H', self.dburl,
                                           '--color=no', '-S', '--utf8')
        self.assertCmdSuccess(result, utf8, err)

        result, ascii, err = self.runsubcmd("visualize", "ntdsconn",
                                            '-H', self.dburl,
                                            '--color=no', '-S')
        self.assertCmdSuccess(result, ascii, err)
        for c in ('│', '─', '╭'):
            self.assertTrue(c in utf8, 'UTF8 should contain %s' % c)
            self.assertTrue(c not in ascii, 'ASCII should not contain %s' % c)

    def test_forced_local_dsa(self):
        # the forced_local_dsa shouldn't make any difference, except
        # for the title line.
        result, target, err = self.runsubcmd("visualize", "ntdsconn",
                                             '-H', self.dburl,
                                             '--color=no', '-S')
        self.assertCmdSuccess(result, target, err)
        files = []
        target = target.strip().split('\n', 1)[1]
        for cn, site in MULTISITE_LDIF_DSAS:
            dsa = DN_TEMPLATE % (cn, site)
            samdb, dbfile = samdb_from_ldif(MULTISITE_LDIF,
                                            self.tempdir,
                                            self.lp, dsa,
                                            tag=cn)

            result, out, err = self.runsubcmd("visualize", "ntdsconn",
                                              '-H', 'tdb://' + dbfile,
                                              '--color=no', '-S')
            self.assertCmdSuccess(result, out, err)
            # Separate out the title line, which will differ in the DN.
            title, body = out.strip().split('\n', 1)
            self.assertStringsEqual(target, body)
            self.assertIn(cn, title)
            files.append(dbfile)
        self.remove_files(*files)

    def test_short_names(self):
        """Ensure the colour ones are the same as the monochrome ones EXCEPT
        for the colours, of which the monochrome one should know nothing"""
        result, short, err = self.runsubcmd("visualize", "ntdsconn",
                                            '-H', self.dburl,
                                            '--color=no', '-S', '--no-key')
        self.assertCmdSuccess(result, short, err)
        result, long, err = self.runsubcmd("visualize", "ntdsconn",
                                           '-H', self.dburl,
                                           '--color=no', '--no-key')
        self.assertCmdSuccess(result, long, err)

        lines = short.split('\n')
        replacements = []
        key_lines = ['']
        short_without_key = []
        for line in lines:
            m = re.match(r"'(.{1,2})' stands for '(.+)'", line)
            if m:
                a, b = m.groups()
                replacements.append((len(a), a, b))
                key_lines.append(line)
            else:
                short_without_key.append(line)

        short = '\n'.join(short_without_key)
        # we need to replace longest strings first
        replacements.sort(reverse=True)
        short2long = short
        # we don't want to shorten the DC name in the header line.
        long_header, long2short = long.strip().split('\n', 1)
        for _, a, b in replacements:
            short2long = short2long.replace(a, b)
            long2short = long2short.replace(b, a)

        long2short = '%s\n%s' % (long_header, long2short)

        # The white space is going to be all wacky, so lets squish it down
        short2long = collapse_space(short2long)
        long2short = collapse_space(long2short)
        short = collapse_space(short)
        long = collapse_space(long)

        self.assertStringsEqual(short2long, long, strip=True)
        self.assertStringsEqual(short, long2short, strip=True)

    def test_disconnected_ldif_with_key(self):
        """Test that the 'unconnected' ldif shows up and exactly matches the
        expected output."""
        # This is not truly a disconnected graph because the
        # vampre/local/promoted DCs are in there and they have
        # relationships, and SERVER2 and SERVER3 for some reason refer
        # to them.

        samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
                                        self.tempdir,
                                        self.lp, tag='disconnected')
        dburl = 'tdb://' + dbfile
        result, output, err = self.runsubcmd("visualize", "ntdsconn",
                                             '-H', dburl,
                                             '--color=no', '-S')
        self.remove_files(dbfile)
        self.assertCmdSuccess(result, output, err)
        self.assertStringsEqual(output,
                                EXPECTED_DISTANCE_GRAPH_WITH_KEY)

    def test_dot_ntdsconn(self):
        """Graphviz NTDS Connection output"""
        result, dot, err = self.runsubcmd("visualize", "ntdsconn",
                                          '-H', self.dburl,
                                          '--color=no', '-S', '--dot',
                                          '--no-key')
        self.assertCmdSuccess(result, dot, err)
        self.assertStringsEqual(EXPECTED_DOT_MULTISITE_NO_KEY, dot)

    def test_dot_ntdsconn_disconnected(self):
        """Graphviz NTDS Connection output from disconnected graph"""
        samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
                                        self.tempdir,
                                        self.lp, tag='disconnected')

        result, dot, err = self.runsubcmd("visualize", "ntdsconn",
                                          '-H', 'tdb://' + dbfile,
                                          '--color=no', '-S', '--dot',
                                          '-o', '-')
        self.assertCmdSuccess(result, dot, err)
        self.remove_files(dbfile)
        self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot,
                                strip=True)

    def test_dot_ntdsconn_disconnected_to_file(self):
        """Graphviz NTDS Connection output into a file"""
        samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
                                        self.tempdir,
                                        self.lp, tag='disconnected')

        dot_file = os.path.join(self.tempdir, 'dotfile')

        result, dot, err = self.runsubcmd("visualize", "ntdsconn",
                                          '-H', 'tdb://' + dbfile,
                                          '--color=no', '-S', '--dot',
                                          '-o', dot_file)
        self.assertCmdSuccess(result, dot, err)
        f = open(dot_file)
        dot = f.read()
        f.close()
        self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot)

        self.remove_files(dbfile, dot_file)


EXPECTED_DOT_MULTISITE_NO_KEY = r"""/* generated by samba */
digraph A_samba_tool_production {
label="NTDS Connections known to CN=WIN01,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=ad,DC=samba,DC=example,DC=com";
fontsize=10;

node[fontname=Helvetica; fontsize=10];

"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n...";
"CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n...";
"CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n...";
"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n...";
"CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n...";
"CN=NTDS Settings,\nCN=WIN06,\nCN=Servers,\nCN=Site-3,\n...";
"CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n...";
"CN=NTDS Settings,\nCN=WIN08,\nCN=Servers,\nCN=Site-4,\n...";
"CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n...";
"CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n...";
"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN06,\nCN=Servers,\nCN=Site-3,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN08,\nCN=Servers,\nCN=Site-4,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n..." -> "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
}

"""


EXPECTED_DOT_NTDSCONN_DISCONNECTED = r"""/* generated by samba */
digraph A_samba_tool_production {
label="NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com";
fontsize=10;

node[fontname=Helvetica; fontsize=10];

"CN=NTDS Settings,\nCN=CLIENT,\n...";
"CN=NTDS Settings,\nCN=LOCALDC,\n...";
"CN=NTDS Settings,\nCN=PROMOTEDVDC,\n...";
"CN=NTDS Settings,\nCN=SERVER1,\n...";
"CN=NTDS Settings,\nCN=SERVER2,\n...";
"CN=NTDS Settings,\nCN=SERVER3,\n...";
"CN=NTDS Settings,\nCN=SERVER4,\n...";
"CN=NTDS Settings,\nCN=SERVER5,\n...";
"CN=NTDS Settings,\nCN=LOCALDC,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=SERVER2,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
"CN=NTDS Settings,\nCN=SERVER3,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
subgraph cluster_key {
label="Key";
subgraph cluster_key_nodes {
label="";
color = "invis";

}
subgraph cluster_key_edges {
label="";
color = "invis";
subgraph cluster_key_0_ {
key_0_e1[label=src; color="#000000"; group="key_0__g"]
key_0_e2[label=dest; color="#000000"; group="key_0__g"]
key_0_e1 -> key_0_e2 [constraint = false; color="#000000"]
key_0__label[shape=plaintext; style=solid; width=2.000000; label="NTDS Connection\r"]
}
{key_0__label}
}

elision0[shape=plaintext; style=solid; label="\“...”  means  “CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com”\r"]

}
"CN=NTDS Settings,\nCN=CLIENT,\n..." -> key_0__label [style=invis];
"CN=NTDS Settings,\nCN=LOCALDC,\n..." -> key_0__label [style=invis];
"CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> key_0__label [style=invis];
"CN=NTDS Settings,\nCN=SERVER1,\n..." -> key_0__label [style=invis];
"CN=NTDS Settings,\nCN=SERVER2,\n..." -> key_0__label [style=invis];
"CN=NTDS Settings,\nCN=SERVER3,\n..." -> key_0__label [style=invis];
"CN=NTDS Settings,\nCN=SERVER4,\n..." -> key_0__label [style=invis];
"CN=NTDS Settings,\nCN=SERVER5,\n..." -> key_0__label [style=invis]
key_0__label -> elision0 [style=invis; weight=9]

}
"""

EXPECTED_DISTANCE_GRAPH_WITH_KEY = """
NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com

                            destination
                  ,-------- *,CN=CLIENT+
                  |,------- *,CN=LOCALDC+
                  ||,------ *,CN=PROMOTEDVDC+
                  |||,----- *,CN=SERVER1+
                  ||||,---- *,CN=SERVER2+
                  |||||,--- *,CN=SERVER3+
                  ||||||,-- *,CN=SERVER4+
           source |||||||,- *,CN=SERVER5+
     *,CN=CLIENT+ 0-------
    *,CN=LOCALDC+ -01-----
*,CN=PROMOTEDVDC+ -10-----
    *,CN=SERVER1+ ---0----
    *,CN=SERVER2+ -21-0---
    *,CN=SERVER3+ -12--0--
    *,CN=SERVER4+ ------0-
    *,CN=SERVER5+ -------0

'*' stands for 'CN=NTDS Settings'
'+' stands for ',CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'

Data can get from source to destination in the indicated number of steps.
0 means zero steps (it is the same DC)
1 means a direct link
2 means a transitive link involving two steps (i.e. one intermediate DC)
- means there is no connection, even through other DCs

"""
