This particular set, though, was legacy code pretty much the week after it was written :(
I had a fun bit of legacy code wrangling a month or so back. I looked at production profiles of some very compute-heavy jobs (we have great tools for that) and found we were spending ~half of my pay on cloud compute in this one library function -- deep equality checks for a kind of object.
The equality-checking code itself is very slow, significantly slower than hand-written alternatives, but the objects are complicated (though luckily tree-like in structure) so hand-writing equality comparison functions would be a nightmare.
Many of these structures had "id" fields, though. Gasp, could we just compare ids instead of doing deep equality checks?
So I asked around, and nobody knew. Nobody even knew what the code was for. It'd be trivial to replace the deep equality checks with id comparisons, but we can't do it.
Seeing how some legacy code works is one thing. Data comes in here, data goes out there. Knowing which invariants the code is trying to maintain, though, what properties it assumes about the data here or there (Sorted? Unique? Up to date? All belongs to the same user?) makes making changes difficult, though. It means you need to write defensive code that the original authors would find ridiculous, because you don't have their institutional knowledge, and can't derive it locally in the code, and can't inspect it at runtime (or be sure that prod won't send a counter-example the day after you deploy to production.)
I don't always do TDD in a 100% strict way. The duration of the TDD cycle varies for me mainly depending on how practical a very short cycle is and also depending on how complex the software is. I have found that overly obsessing about very short cycles can be inefficient. Also, I tend to test a cluster of multiple classes instead of a single class or a single method. I think the subject that a test is about should be something at a level that is a bit higher, perhaps even something that the customer could recognize as something they value. If you do not do that you might be testing implementation details that are very much subject to change. Testing at the right level also can save quite a bit of time.
I don't write user interfaces that often and when I do they are mostly not that fancy. In that case I have sometimes written tests that check that the HTML output is correct. In the case of things that are purely visible I quite often first fix the thing and then write a test because the browser in my head is not always quite good enough to write a correct test beforehand.
I have generally found that management was encouraging towards writing test in places where I have worked. Occasionally I have been praised for writing high quality software but also sometimes got the remark that sometimes things were taking a longer than expected. Everybody should be able to understand that there is a trade-off here. I would not want to work in a place where they produce as much crap as possible in as little time as possible. Lack of quality is quite hard on me emotionally.
I think it's this glossing over the inherent and deep complexity of software development that turns a lot of people off TDD.
What's wrong with getting the software right, often through a process of trial and error, and then writing the tests to lock in what you have working?
One might write test afterwards but as I note above tests do cost time so one should aim for the maximum payback of that time. When you start with the test it starts paying back as soon as possible starting from the point where it flags your first attempt making the test pass as not quite working.
One case where it may be better to start with some trial and error is when it is not clear what algorithm should be used.
This is what I was referring to.